├── .meteor
├── .gitignore
├── release
├── platforms
├── .id
├── .finished-upgraders
├── packages
└── versions
├── src
├── server.js
├── client
│ ├── modules
│ │ ├── core
│ │ │ ├── actions
│ │ │ │ ├── index.js
│ │ │ │ ├── posts.js
│ │ │ │ └── tests
│ │ │ │ │ └── posts.js
│ │ │ ├── configs
│ │ │ │ └── method_stubs
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── posts.js
│ │ │ ├── index.js
│ │ │ ├── components
│ │ │ │ ├── navigations.jsx
│ │ │ │ ├── postlist.jsx
│ │ │ │ ├── post.jsx
│ │ │ │ ├── layout.main.jsx
│ │ │ │ ├── tests
│ │ │ │ │ ├── layout.main.js
│ │ │ │ │ ├── navigations.js
│ │ │ │ │ ├── post.js
│ │ │ │ │ ├── postlist.js
│ │ │ │ │ └── newpost.js
│ │ │ │ ├── newpost.jsx
│ │ │ │ └── style.css
│ │ │ ├── containers
│ │ │ │ ├── postlist.js
│ │ │ │ ├── newpost.js
│ │ │ │ ├── tests
│ │ │ │ │ ├── newpost.js
│ │ │ │ │ ├── postlist.js
│ │ │ │ │ └── post.js
│ │ │ │ └── post.js
│ │ │ └── routes.jsx
│ │ └── comments
│ │ │ ├── configs
│ │ │ └── method_stubs
│ │ │ │ ├── index.js
│ │ │ │ └── comments.js
│ │ │ ├── index.js
│ │ │ ├── actions
│ │ │ ├── index.js
│ │ │ └── comments.js
│ │ │ ├── components
│ │ │ ├── style.css
│ │ │ ├── comment_list.jsx
│ │ │ └── create_comment.jsx
│ │ │ └── containers
│ │ │ ├── comment_list.js
│ │ │ └── create_comment.js
│ ├── main.js
│ └── configs
│ │ └── context.js
├── server
│ ├── methods
│ │ ├── index.js
│ │ └── posts.js
│ ├── publications
│ │ ├── index.js
│ │ └── posts.js
│ ├── main.js
│ └── configs
│ │ └── initial_adds.js
├── client.js
├── lib
│ └── collections.js
└── meteor.startup.js
├── .gitignore
├── .babeldc
├── mantra.cson
├── webpack.json
├── .editorconfig
├── .scripts
└── mocha_boot.js
├── wallaby.js
├── README.md
├── package.json
└── .eslintrc
/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.3.2.4
2 |
--------------------------------------------------------------------------------
/.meteor/platforms:
--------------------------------------------------------------------------------
1 | browser
2 | server
3 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import './server/main';
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.babeldc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["jsx-control-statements"]
3 | }
4 |
--------------------------------------------------------------------------------
/mantra.cson:
--------------------------------------------------------------------------------
1 | 'language': 'js',
2 | 'root': 'src',
3 | 'libFolderName': 'lib'
4 |
--------------------------------------------------------------------------------
/webpack.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "src",
3 | "devServer": {
4 | "host": "localhost"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/client/modules/core/actions/index.js:
--------------------------------------------------------------------------------
1 | import posts from './posts';
2 |
3 | export default {
4 | posts
5 | };
6 |
--------------------------------------------------------------------------------
/src/server/methods/index.js:
--------------------------------------------------------------------------------
1 | import posts from './posts';
2 |
3 | export default function () {
4 | posts();
5 | }
6 |
--------------------------------------------------------------------------------
/src/server/publications/index.js:
--------------------------------------------------------------------------------
1 | import posts from './posts';
2 |
3 | export default function () {
4 | posts();
5 | }
6 |
--------------------------------------------------------------------------------
/src/client/modules/core/configs/method_stubs/index.js:
--------------------------------------------------------------------------------
1 | import posts from './posts';
2 |
3 | export default function (context) {
4 | posts(context);
5 | }
6 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | import { FlowRouter } from 'meteor/kadira:flow-router';
2 |
3 | import './client/main';
4 |
5 | Meteor.startup(function() {
6 | FlowRouter.initialize();
7 | });
8 |
--------------------------------------------------------------------------------
/src/lib/collections.js:
--------------------------------------------------------------------------------
1 | import {Mongo} from 'meteor/mongo';
2 |
3 | export const Posts = new Mongo.Collection('posts');
4 | export const Comments = new Mongo.Collection('comments');
5 |
--------------------------------------------------------------------------------
/src/meteor.startup.js:
--------------------------------------------------------------------------------
1 | // This file is sent directly to Meteor without going through Webpack
2 | // You can initialize anything you need before your app start here
3 |
4 |
5 | FlowRouter.wait();
6 |
7 |
--------------------------------------------------------------------------------
/src/client/modules/comments/configs/method_stubs/index.js:
--------------------------------------------------------------------------------
1 | import comments from './comments';
2 |
3 | // XXX: Here we can auto generate this file based on the method stubs
4 | export default function (context) {
5 | comments(context);
6 | }
7 |
--------------------------------------------------------------------------------
/src/client/modules/comments/index.js:
--------------------------------------------------------------------------------
1 | import methodStubs from './configs/method_stubs';
2 | import actions from './actions';
3 |
4 | export default {
5 | actions,
6 | load(context) {
7 | methodStubs(context);
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/client/modules/comments/actions/index.js:
--------------------------------------------------------------------------------
1 | import comments from './comments';
2 |
3 | // XXX: Here, we can automatically generate this file based on the
4 | // actions inside this directory.
5 | const actions = {
6 | comments
7 | };
8 |
9 | export default actions;
10 |
--------------------------------------------------------------------------------
/src/client/modules/core/index.js:
--------------------------------------------------------------------------------
1 | import methodStubs from './configs/method_stubs';
2 | import actions from './actions';
3 | import routes from './routes.jsx';
4 |
5 | export default {
6 | routes,
7 | actions,
8 | load(context) {
9 | methodStubs(context);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/navigations.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Navigations = () => (
4 |
9 | );
10 |
11 | export default Navigations;
12 |
--------------------------------------------------------------------------------
/src/server/main.js:
--------------------------------------------------------------------------------
1 | import addInitialData from './configs/initial_adds.js';
2 |
3 | addInitialData();
4 |
5 | // import publications and methods
6 |
7 | import publications from './publications/index';
8 | import methods from './methods/index';
9 |
10 | publications();
11 | methods();
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # @see http://editorconfig.org/
2 |
3 | # This is the top-most .editorconfig file; do not search in parent directories.
4 | root = true
5 |
6 | # All files.
7 | [*]
8 | end_of_line = LF
9 | indent_style = space
10 | indent_size = 2
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | a247lv2k0c6tl3kml5
8 |
--------------------------------------------------------------------------------
/src/client/modules/comments/components/style.css:
--------------------------------------------------------------------------------
1 | .comments {
2 | font-size: 13px;
3 | }
4 |
5 | .comments textarea {
6 | width: 250px;
7 | outline: 0;
8 | border: 1px solid #ddd;
9 | padding: 5px 5px;
10 | }
11 |
12 | .comments .comment-list {
13 | margin-top: 15px;
14 | }
15 |
16 | .comments .error {
17 | color: red;
18 | padding: 5px 0;
19 | }
20 |
--------------------------------------------------------------------------------
/src/server/configs/initial_adds.js:
--------------------------------------------------------------------------------
1 | import {Posts} from 'lib/collections';
2 |
3 | export default function () {
4 | if (!Posts.findOne()) {
5 | for (let lc = 1; lc <= 5; lc++) {
6 | const title = `This is the post title: ${lc}`;
7 | const content = `Post ${lc}'s content is great!`;
8 | Posts.insert({title, content});
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/postlist.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PostList = ({posts}) => (
4 |
5 |
6 | {posts.map(post => (
7 | -
8 | {post.title}
9 |
10 | ))}
11 |
12 |
13 | );
14 |
15 | export default PostList;
16 |
--------------------------------------------------------------------------------
/src/client/modules/comments/configs/method_stubs/comments.js:
--------------------------------------------------------------------------------
1 | export default function ({Collections, Meteor}) {
2 | Meteor.methods({
3 | 'posts.createComment'(_id, postId, text) {
4 | const saving = true;
5 | const createdAt = new Date();
6 | const author = 'Me';
7 | Collections.Comments.insert({
8 | _id, postId, text, saving, createdAt, author
9 | });
10 | }
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/client/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'mantra-core';
2 | import initContext from './configs/context';
3 |
4 | // modules
5 | import coreModule from './modules/core';
6 | import commentsModule from './modules/comments';
7 |
8 | // init context
9 | const context = initContext();
10 |
11 | // create app
12 | const app = createApp(context);
13 | app.loadModule(coreModule);
14 | app.loadModule(commentsModule);
15 | app.init();
16 |
--------------------------------------------------------------------------------
/src/client/configs/context.js:
--------------------------------------------------------------------------------
1 | import * as Collections from 'lib/collections';
2 | import {Meteor} from 'meteor/meteor';
3 | import {FlowRouter} from 'meteor/kadira:flow-router';
4 | import {ReactiveDict} from 'meteor/reactive-dict';
5 | import {Tracker} from 'meteor/tracker';
6 |
7 | export default function () {
8 | return {
9 | Meteor,
10 | FlowRouter,
11 | Collections,
12 | LocalState: new ReactiveDict(),
13 | Tracker
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/post.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CommentList from 'client/modules/comments/containers/comment_list.js';
3 |
4 | const Post = ({post}) => (
5 |
6 | {post.saving ?
Saving...
: null}
7 |
{post.title}
8 |
9 | {post.content}
10 |
11 |
12 |
Comments
13 |
14 |
15 |
16 | );
17 |
18 | export default Post;
19 |
--------------------------------------------------------------------------------
/.scripts/mocha_boot.js:
--------------------------------------------------------------------------------
1 | var jsdom = require('jsdom').jsdom;
2 |
3 | var exposedProperties = ['window', 'navigator', 'document'];
4 |
5 | global.document = jsdom('');
6 | global.window = document.defaultView;
7 | Object.keys(document.defaultView).forEach((property) => {
8 | if (typeof global[property] === 'undefined') {
9 | exposedProperties.push(property);
10 | global[property] = document.defaultView[property];
11 | }
12 | });
13 |
14 | global.navigator = {
15 | userAgent: 'node.js'
16 | };
17 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/layout.main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Navigations from './navigations.jsx';
3 |
4 | const Layout = ({content = () => null }) => (
5 |
6 |
10 |
11 |
12 | {content()}
13 |
14 |
15 |
18 |
19 | );
20 |
21 | export default Layout;
22 |
--------------------------------------------------------------------------------
/src/client/modules/core/containers/postlist.js:
--------------------------------------------------------------------------------
1 | import PostList from '../components/postlist.jsx';
2 | import {useDeps, composeWithTracker, composeAll} from 'mantra-core';
3 |
4 | export const composer = ({context}, onData) => {
5 | const {Meteor, Collections} = context();
6 | if (Meteor.subscribe('posts.list').ready()) {
7 | const posts = Collections.Posts.find().fetch();
8 | onData(null, {posts});
9 | }
10 | };
11 |
12 | export default composeAll(
13 | composeWithTracker(composer),
14 | useDeps()
15 | )(PostList);
16 |
--------------------------------------------------------------------------------
/src/client/modules/core/configs/method_stubs/posts.js:
--------------------------------------------------------------------------------
1 | import {check} from 'meteor/check';
2 |
3 | export default function ({Meteor, Collections}) {
4 | Meteor.methods({
5 | 'posts.create'(_id, title, content) {
6 | check(_id, String);
7 | check(title, String);
8 | check(content, String);
9 |
10 | const createdAt = new Date();
11 | const post = {
12 | _id, title, content, createdAt,
13 | saving: true
14 | };
15 |
16 | Collections.Posts.insert(post);
17 | }
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/client/modules/comments/actions/comments.js:
--------------------------------------------------------------------------------
1 | export default {
2 | create({Meteor, LocalState}, postId, text) {
3 | LocalState.set('CREATE_COMMENT_ERROR', null);
4 | if (!text) {
5 | LocalState.set('CREATE_COMMENT_ERROR', 'Comment text is required.');
6 | return;
7 | }
8 |
9 | const id = Meteor.uuid();
10 | Meteor.call('posts.createComment', id, postId, text, (err) => {
11 | if (err) {
12 | alert(`Post creating failed: ${err.message}`);
13 | }
14 | });
15 | },
16 |
17 | clearErrors({LocalState}) {
18 | return LocalState.set('CREATE_COMMENT_ERROR', null);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/src/client/modules/comments/components/comment_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CreateComment from '../containers/create_comment.js';
3 |
4 | const CommentList = ({comments, postId}) => (
5 |
6 |
7 |
8 |
9 |
10 | {comments.length === 0 ?
No Comments Yet!
: null}
11 | {comments.map(comment => (
12 |
13 | {comment.author}: {comment.text}
14 | {comment.saving ? '...' : null}
15 |
16 | ))}
17 |
18 |
19 | );
20 |
21 | export default CommentList;
22 |
--------------------------------------------------------------------------------
/src/client/modules/comments/containers/comment_list.js:
--------------------------------------------------------------------------------
1 | import {
2 | useDeps, composeWithTracker, composeAll
3 | } from 'mantra-core';
4 | import Component from '../components/comment_list.jsx';
5 |
6 | export const composer = ({context, clearErrors, postId}, onData) => {
7 | const {Meteor, Collections} = context();
8 | if (Meteor.subscribe('posts.comments', postId).ready()) {
9 | const options = {
10 | sort: {createdAt: -1}
11 | };
12 | const comments = Collections.Comments.find({postId}, options).fetch();
13 | onData(null, {comments});
14 | } else {
15 | onData();
16 | }
17 | };
18 |
19 | export default composeAll(
20 | composeWithTracker(composer),
21 | useDeps()
22 | )(Component);
23 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/tests/layout.main.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {shallow} from 'enzyme';
4 | import MainLayout from '../layout.main.jsx';
5 | import Navigations from '../navigations.jsx';
6 |
7 | describe('components.layouts.main', () => {
8 | it('should contain navigations', () => {
9 | const el = shallow();
10 | expect(el.contains()).to.be.equal(true);
11 | });
12 |
13 | it('should render childrens', () => {
14 | const Comp = () => (Hello
);
15 | const el = shallow(
16 | ()}/>
17 | );
18 |
19 | expect(el.contains()).to.be.equal(true);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/client/modules/comments/containers/create_comment.js:
--------------------------------------------------------------------------------
1 | import {
2 | useDeps, composeWithTracker, composeAll
3 | } from 'mantra-core';
4 | import Component from '../components/create_comment.jsx';
5 |
6 | export const composer = ({context, clearErrors}, onData) => {
7 | const {LocalState} = context();
8 | const error = LocalState.get('CREATE_COMMENT_ERROR');
9 | onData(null, {error});
10 |
11 | return clearErrors;
12 | };
13 |
14 | export const depsMapper = (context, actions) => ({
15 | create: actions.comments.create,
16 | clearErrors: actions.comments.clearErrors,
17 | context: () => context
18 | });
19 |
20 | export default composeAll(
21 | composeWithTracker(composer),
22 | useDeps(depsMapper)
23 | )(Component);
24 |
--------------------------------------------------------------------------------
/src/client/modules/core/containers/newpost.js:
--------------------------------------------------------------------------------
1 | import NewPost from '../components/newpost.jsx';
2 | import {useDeps, composeWithTracker, composeAll} from 'mantra-core';
3 |
4 | export const composer = ({context, clearErrors}, onData) => {
5 | const {LocalState} = context();
6 | const error = LocalState.get('SAVING_ERROR');
7 | onData(null, {error});
8 |
9 | // clearErrors when unmounting the component
10 | return clearErrors;
11 | };
12 |
13 | export const depsMapper = (context, actions) => ({
14 | create: actions.posts.create,
15 | clearErrors: actions.posts.clearErrors,
16 | context: () => context
17 | });
18 |
19 | export default composeAll(
20 | composeWithTracker(composer),
21 | useDeps(depsMapper)
22 | )(NewPost);
23 |
--------------------------------------------------------------------------------
/src/client/modules/core/actions/posts.js:
--------------------------------------------------------------------------------
1 | export default {
2 | create({Meteor, LocalState, FlowRouter}, title, content) {
3 | if (!title || !content) {
4 | return LocalState.set('SAVING_ERROR', 'Title & Content are required!');
5 | }
6 |
7 | LocalState.set('SAVING_ERROR', null);
8 |
9 | const id = Meteor.uuid();
10 | // There is a method stub for this in the config/method_stubs
11 | // That's how we are doing latency compensation
12 | Meteor.call('posts.create', id, title, content, (err) => {
13 | if (err) {
14 | return LocalState.set('SAVING_ERROR', err.message);
15 | }
16 | });
17 | FlowRouter.go(`/post/${id}`);
18 | },
19 |
20 | clearErrors({LocalState}) {
21 | return LocalState.set('SAVING_ERROR', null);
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/tests/navigations.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {shallow} from 'enzyme';
4 | import Navigations from '../navigations.jsx';
5 |
6 | describe('components.navigations', () => {
7 | it('should contain a link to home', () => {
8 | const el = shallow();
9 | const homeLink = el.find('a').at(0);
10 | expect(homeLink.text()).to.be.equal('Home');
11 | expect(homeLink.prop('href')).to.be.equal('/');
12 | });
13 |
14 | it('should contain a link to create a new post', () => {
15 | const el = shallow();
16 | const newPostLink = el.find('a').at(1);
17 | expect(newPostLink.text()).to.be.equal('New Post');
18 | expect(newPostLink.prop('href')).to.be.equal('/new-post');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/server/publications/posts.js:
--------------------------------------------------------------------------------
1 | import {Posts, Comments} from 'lib/collections';
2 | import {Meteor} from 'meteor/meteor';
3 | import {check} from 'meteor/check';
4 |
5 | export default function () {
6 | Meteor.publish('posts.list', function () {
7 | const selector = {};
8 | const options = {
9 | fields: {_id: 1, title: 1},
10 | sort: {createdAt: -1},
11 | limit: 10
12 | };
13 |
14 | return Posts.find(selector, options);
15 | });
16 |
17 | Meteor.publish('posts.single', function (postId) {
18 | check(postId, String);
19 | const selector = {_id: postId};
20 | return Posts.find(selector);
21 | });
22 |
23 | Meteor.publish('posts.comments', function (postId) {
24 | check(postId, String);
25 | const selector = {postId};
26 | return Comments.find(selector);
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/newpost.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class NewPost extends React.Component {
4 | render() {
5 | const {error} = this.props;
6 | return (
7 |
8 |
Add New Post
9 | {error ?
{error}
: null}
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | createPost() {
19 | const {create} = this.props;
20 | const {titleRef, contentRef} = this.refs;
21 |
22 | create(titleRef.value, contentRef.value);
23 | }
24 | }
25 |
26 | export default NewPost;
27 |
--------------------------------------------------------------------------------
/src/client/modules/core/containers/tests/newpost.js:
--------------------------------------------------------------------------------
1 | // We need require React from NPM before working on these tests
2 |
3 | // const { describe, it } = global;
4 | // import {expect} from 'chai';
5 | // import {stub, spy} from 'sinon';
6 | // import {composer, depsMapper} from '../newpost';
7 |
8 | // describe('containers.newpost', () => {
9 | // describe('composer', () => {
10 | // it('should get SAVING_ERROR from local state');
11 | // it('should get SAVING_NEW_POST from local state');
12 | // });
13 |
14 | // describe('depsMapper', () => {
15 | // describe('actions', () => {
16 | // it('should map posts.create');
17 | // it('should map posts.clearErrors');
18 | // });
19 | // describe('context', () => {
20 | // it('should map the whole context as a function');
21 | // });
22 | // });
23 | // });
24 |
--------------------------------------------------------------------------------
/src/client/modules/comments/components/create_comment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class CreateComment extends React.Component {
4 | render() {
5 | const {error} = this.props;
6 | return (
7 |
8 | {error ? this._renderError(error) : null}
9 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | _create() {
19 | const text = this.refs.text.value;
20 | const {create, postId} = this.props;
21 | create(postId, text);
22 | this.refs.text.value = '';
23 | }
24 |
25 | _renderError(error) {
26 | return (
27 |
28 | {error}
29 |
30 | );
31 | }
32 | }
33 |
34 | export default CreateComment;
35 |
--------------------------------------------------------------------------------
/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base # Packages every Meteor app needs to have
8 | mobile-experience # Packages for a great mobile UX
9 | mongo # The database Meteor supports right now
10 | jquery # Helpful client-side library
11 | tracker # Meteor's client-side reactive programming library
12 |
13 | es5-shim # ECMAScript 5 compatibility for older browsers.
14 |
15 | react-runtime
16 |
17 | webpack:webpack
18 | webpack:react
19 | webpack:css
20 | webpack:assets
21 |
22 | accounts-password
23 | kadira:flow-router
24 | standard-minifier-css
25 | standard-minifier-js
26 | react-meteor-data
--------------------------------------------------------------------------------
/src/client/modules/core/components/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif;
3 | padding: 10px 20px;
4 | }
5 |
6 | header {
7 | border-bottom: 1px solid #EAEAEA;
8 | padding-bottom: 10px;
9 | margin-bottom: 30px;
10 | }
11 |
12 | footer {
13 | margin-top: 50px;
14 | border-top: 1px solid #EAEAEA;
15 | padding-top: 10px;
16 | font-size: 12px;
17 | }
18 |
19 | .postlist li {
20 | margin: 8px 0;
21 | }
22 |
23 | .postlist li a {
24 | text-decoration: none;
25 | }
26 |
27 | .new-post input,
28 | .new-post textarea {
29 | width: 350px;
30 | outline: 0;
31 | margin-bottom: 5px;
32 | border: 1px solid #ddd;
33 | font-size: 13px;
34 | padding: 4px 6px;
35 | }
36 |
37 | .new-post textarea {
38 | height: 200px;
39 | }
40 |
41 | footer {
42 | margin-top: 20px;
43 | border-top: 1px solid #eee;
44 | padding-top: 5px;
45 | }
46 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/tests/post.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {shallow} from 'enzyme';
4 | import Post from '../post.jsx';
5 |
6 | describe('components.post', () => {
7 | it('should display the post title', () => {
8 | const post = {title: 'Nice One'};
9 | const el = shallow();
10 | expect(el.find('h2').text()).to.be.match(/Nice One/);
11 | });
12 |
13 | it('should display the post content', () => {
14 | const post = {content: 'Nice content'};
15 | const el = shallow();
16 | expect(el.find('p').text()).to.be.match(/Nice content/);
17 | });
18 |
19 | it('should display saving indicator if saving prop is there', () => {
20 | const post = {saving: true};
21 | const el = shallow();
22 | expect(el.find('p').first().text()).to.be.match(/saving/i);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/client/modules/core/containers/post.js:
--------------------------------------------------------------------------------
1 | import Post from '../components/post.jsx';
2 | import {useDeps, composeWithTracker, composeAll} from 'mantra-core';
3 |
4 | export const composer = ({context, postId}, onData) => {
5 | const {Meteor, Collections, Tracker} = context();
6 |
7 | Meteor.subscribe('posts.single', postId, () => {
8 | const post = Collections.Posts.findOne(postId);
9 | onData(null, {post});
10 | });
11 |
12 | // support latency compensation
13 | // we don't need to invalidate tracker because of the
14 | // data fetching from the cache.
15 | const postFromCache = Tracker.nonreactive(() => {
16 | return Collections.Posts.findOne(postId);
17 | });
18 |
19 | if (postFromCache) {
20 | onData(null, {post: postFromCache});
21 | } else {
22 | onData();
23 | }
24 | };
25 |
26 | export default composeAll(
27 | composeWithTracker(composer),
28 | useDeps()
29 | )(Post);
30 |
--------------------------------------------------------------------------------
/wallaby.js:
--------------------------------------------------------------------------------
1 | module.exports = function (wallaby) {
2 | // There is a weird error with the mui and mantra.
3 | // See: https://goo.gl/cLH8ib
4 | // Using require here seems to be the error.
5 | // Renaming it into `load` just fixed the issue.
6 | var load = require;
7 |
8 | return {
9 | files: [
10 | 'client/modules/**/components/*.jsx',
11 | 'client/modules/**/actions/*.js',
12 | 'client/modules/**/containers/*.js',
13 | 'client/modules/**/libs/*.js'
14 | ],
15 | tests: [
16 | 'client/**/tests/*.js'
17 | ],
18 | compilers: {
19 | '**/*.js*': wallaby.compilers.babel({
20 | babel: load('babel-core'),
21 | presets: ['es2015', 'stage-2', 'react']
22 | })
23 | },
24 | env: {
25 | type: 'node'
26 | },
27 | testFramework: 'mocha',
28 | setup: function() {
29 | global.React = require('react');
30 | }
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/src/server/methods/posts.js:
--------------------------------------------------------------------------------
1 | import {Posts, Comments} from 'lib/collections';
2 | import {Meteor} from 'meteor/meteor';
3 | import {check} from 'meteor/check';
4 |
5 | export default function () {
6 | Meteor.methods({
7 | 'posts.create'(_id, title, content) {
8 | check(_id, String);
9 | check(title, String);
10 | check(content, String);
11 |
12 | // Show the latency compensations
13 | Meteor._sleepForMs(500);
14 |
15 | // XXX: Do some user authorization
16 | const createdAt = new Date();
17 | const post = {_id, title, content, createdAt};
18 | Posts.insert(post);
19 | }
20 | });
21 |
22 | Meteor.methods({
23 | 'posts.createComment'(_id, postId, text) {
24 | check(_id, String);
25 | check(postId, String);
26 | check(text, String);
27 |
28 | // Show the latency compensations
29 | Meteor._sleepForMs(500);
30 |
31 | // XXX: Do some user authorization
32 | const createdAt = new Date();
33 | const author = 'The User';
34 | const comment = {_id, postId, author, text, createdAt};
35 | Comments.insert(comment);
36 | }
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/tests/postlist.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {shallow} from 'enzyme';
4 | import PostList from '../postlist.jsx';
5 |
6 | describe('components.postlist', () => {
7 | const posts = [
8 | {title: 't-one', _id: 'one'},
9 | {title: 't-two', _id: 'two'},
10 | ];
11 |
12 | it('should list given number of items', () => {
13 | const el = shallow();
14 | expect(el.find('li').length).to.be.equal(posts.length);
15 | });
16 |
17 | it('should list post title for each item', () => {
18 | const el = shallow();
19 | const lis = el.find('li');
20 | lis.forEach((li, index) => {
21 | const aText = li.find('a').first().text();
22 | expect(aText).to.be.equal(posts[index].title);
23 | });
24 | });
25 |
26 | it('shallow list post link for each items', () => {
27 | const el = shallow();
28 | const lis = el.find('li');
29 | lis.forEach((li, index) => {
30 | const href = li.find('a').first().prop('href');
31 | expect(href).to.be.equal(`/post/${posts[index]._id}`);
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/client/modules/core/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {mount} from 'react-mounter';
3 |
4 | import {FlowRouter} from 'meteor/kadira:flow-router';
5 | import MainLayout from 'client/modules/core/components/layout.main.jsx';
6 | import PostList from 'client/modules/core/containers/postlist';
7 | import Post from 'client/modules/core/containers/post';
8 | import NewPost from 'client/modules/core/containers/newpost';
9 |
10 | export default function (injectDeps) {
11 | const MainLayoutCtx = injectDeps(MainLayout);
12 |
13 | // Move these as a module and call this from a main file
14 | FlowRouter.route('/', {
15 | name: 'posts.list',
16 | action() {
17 | mount(MainLayoutCtx, {
18 | content: () => ()
19 | });
20 | }
21 | });
22 |
23 | FlowRouter.route('/post/:postId', {
24 | name: 'posts.single',
25 | action({postId}) {
26 | mount(MainLayoutCtx, {
27 | content: () => ()
28 | });
29 | }
30 | });
31 |
32 | FlowRouter.route('/new-post', {
33 | name: 'newpost',
34 | action() {
35 | mount(MainLayoutCtx, {
36 | content: () => ()
37 | });
38 | }
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/src/client/modules/core/containers/tests/postlist.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {stub, spy} from 'sinon';
4 | import {composer} from '../postlist';
5 |
6 | describe('containers.postlist', () => {
7 | describe('composer', () => {
8 | it('should subscribe to posts.list', () => {
9 | const Meteor = {subscribe: stub()};
10 | Meteor.subscribe.returns({ready: () => false});
11 |
12 | const context = () => ({Meteor});
13 | const onData = spy();
14 |
15 | composer({context}, onData);
16 | expect(Meteor.subscribe.args[0]).to.deep.equal([
17 | 'posts.list'
18 | ]);
19 | });
20 |
21 | describe('after subscribed', () => {
22 | it('should fetch data from all posts & pass to onData', () => {
23 | const Meteor = {subscribe: stub()};
24 | Meteor.subscribe.returns({ready: () => true});
25 |
26 | const posts = [ {_id: 'aa'} ];
27 | const Collections = {Posts: {find: stub()}};
28 | Collections.Posts.find.returns({fetch: () => posts});
29 |
30 | const context = () => ({Meteor, Collections});
31 | const onData = spy();
32 |
33 | composer({context}, onData);
34 | expect(onData.args[0]).to.deep.equal([ null, {posts} ]);
35 | });
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/client/modules/core/components/tests/newpost.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {shallow} from 'enzyme';
4 | import NewPost from '../newpost.jsx';
5 |
6 | describe('components.newpost', () => {
7 | it('should show the error if there are any', () => {
8 | const error = 'TheError';
9 | const el = shallow();
10 | expect(el.html()).to.match(/TheError/);
11 | });
12 |
13 | it('should display the create post form', () => {
14 | const el = shallow();
15 | const title = el.find('input').first();
16 | const content = el.find('textarea').first();
17 | const button = el.find('button').first();
18 |
19 | expect(title.node.ref).to.be.equal('titleRef');
20 | expect(content.node.ref).to.be.equal('contentRef');
21 | expect(button.prop('onClick')).to.be.a('function');
22 | });
23 |
24 | it('should create a new post when click on the button', done => {
25 | const title = 'the-title';
26 | const content = 'the-content';
27 |
28 | const onCreate = (t, c) => {
29 | expect(t).to.be.equal(title);
30 | expect(c).to.be.equal(content);
31 | done();
32 | };
33 |
34 | const el = shallow();
35 | const instance = el.instance();
36 |
37 | instance.refs = {
38 | titleRef: {value: title},
39 | contentRef: {value: content}
40 | };
41 |
42 | el.find('button').simulate('click');
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kickstart-mantrajs-webpack
2 |
3 | Kickstart a project with Meteor, Mantra, React and FlowRouter fast!
4 |
5 | 1. git clone https://github.com/mantrajs/kickstart-mantrajs-webpack.git
6 | 1. cd kickstart-mantrajs-webpack
7 | 1. npm install
8 | 1. meteor
9 |
10 | You can try [other kickstart projects](https://github.com/thereactivestack/kickstart) with ReactRouter, FlowRouter or Redux.
11 |
12 | # Hot Module Reload (HMR)
13 |
14 | Mantra prefers to use stateless (bare) react components. These components do not support HMR. If you want to use HRM, you need to use standard React components:
15 |
16 | ```javascript
17 | // NO support for HRM
18 | const Navigations = () => (
19 |
20 | Navigations:
21 |
22 | );
23 |
24 | // FULL Support for HRM
25 | class NewPost extends React.Component {
26 | render() {
27 | return Text
;
28 | }
29 | }
30 | ```
31 |
32 | # Production
33 | You can use meteor run, meteor build, mup or anything working with Meteor.
34 |
35 | ## Run in production mode
36 | `meteor run --production`
37 |
38 | ## Build for production
39 | `meteor build .`
40 |
41 | ## Deploy with Meteor-up
42 | `mup deploy`
43 |
44 | # Cordova
45 | You need to do those 3 steps to make it works with iOS or Android:
46 |
47 | 1. Add the platform to your Meteor project
48 |
49 | ```javascript
50 | meteor add-platform ios
51 | meteor add-platform android
52 | ```
53 | 1. Allow access to your dev server in your `/mobile-config.js` file:
54 |
55 | ```javascript
56 | App.accessRule('http://192.168.1.100:3500/*');
57 | ```
58 |
59 | 1. Replace localhost by your local ip address in `webpack.json`.
60 |
--------------------------------------------------------------------------------
/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.2.7
2 | accounts-password@1.1.8
3 | allow-deny@1.0.4
4 | autoupdate@1.2.9
5 | babel-compiler@6.6.4
6 | babel-runtime@0.1.8
7 | base64@1.0.8
8 | binary-heap@1.0.8
9 | blaze@2.1.7
10 | blaze-tools@1.0.8
11 | boilerplate-generator@1.0.8
12 | callback-hook@1.0.8
13 | check@1.2.1
14 | ddp@1.2.5
15 | ddp-client@1.2.7
16 | ddp-common@1.2.5
17 | ddp-rate-limiter@1.0.4
18 | ddp-server@1.2.6
19 | deps@1.0.12
20 | diff-sequence@1.0.5
21 | ecmascript@0.4.3
22 | ecmascript-runtime@0.2.10
23 | ejson@1.0.11
24 | email@1.0.12
25 | es5-shim@4.5.10
26 | fastclick@1.0.11
27 | geojson-utils@1.0.8
28 | hot-code-push@1.0.4
29 | html-tools@1.0.9
30 | htmljs@1.0.9
31 | http@1.1.5
32 | id-map@1.0.7
33 | jquery@1.11.8
34 | kadira:flow-router@2.12.1
35 | launch-screen@1.0.11
36 | livedata@1.0.18
37 | localstorage@1.0.9
38 | logging@1.0.12
39 | meteor@1.1.14
40 | meteor-base@1.0.4
41 | minifier-css@1.1.11
42 | minifier-js@1.1.11
43 | minimongo@1.0.16
44 | mobile-experience@1.0.4
45 | mobile-status-bar@1.0.12
46 | modules@0.6.1
47 | modules-runtime@0.6.3
48 | mongo@1.1.7
49 | mongo-id@1.0.4
50 | npm-bcrypt@0.8.5
51 | npm-mongo@1.4.43
52 | observe-sequence@1.0.11
53 | ordered-dict@1.0.7
54 | promise@0.6.7
55 | random@1.0.9
56 | rate-limit@1.0.4
57 | react-meteor-data@0.2.9
58 | react-runtime@15.0.1
59 | reactive-dict@1.1.7
60 | reactive-var@1.0.9
61 | reload@1.1.8
62 | retry@1.0.7
63 | routepolicy@1.0.10
64 | service-configuration@1.0.9
65 | sha@1.0.7
66 | spacebars@1.0.11
67 | spacebars-compiler@1.0.11
68 | srp@1.0.8
69 | standard-minifier-css@1.0.6
70 | standard-minifier-js@1.0.6
71 | tmeasday:check-npm-versions@0.3.1
72 | tracker@1.0.13
73 | ui@1.0.11
74 | underscore@1.0.8
75 | url@1.0.9
76 | webapp@1.2.8
77 | webapp-hashing@1.0.9
78 | webpack:assets@1.0.1
79 | webpack:core-config@1.0.1
80 | webpack:css@1.1.1
81 | webpack:json@1.0.1
82 | webpack:npmworkaround@1.0.0
83 | webpack:react@1.2.2
84 | webpack:reload@1.0.1
85 | webpack:webpack@1.3.2
86 |
--------------------------------------------------------------------------------
/src/client/modules/core/containers/tests/post.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {stub, spy} from 'sinon';
4 | import {composer} from '../post';
5 |
6 | describe('containers.post', () => {
7 | describe('composer', () => {
8 | const Tracker = {nonreactive: cb => cb()};
9 | const getCollections = (post) => {
10 | const Collections = {
11 | Posts: {findOne: stub()}
12 | };
13 | Collections.Posts.findOne.returns(post);
14 | return Collections;
15 | };
16 |
17 | it('should subscribe to the given postId via prop', () => {
18 | const Meteor = {subscribe: stub()};
19 | Meteor.subscribe.returns({ready: () => false});
20 | const Collections = getCollections();
21 |
22 | const context = () => ({Meteor, Tracker, Collections});
23 | const postId = 'dwd';
24 | const onData = spy();
25 |
26 | composer({context, postId}, onData);
27 | const args = Meteor.subscribe.args[0];
28 | expect(args.slice(0, 2)).to.deep.equal([
29 | 'posts.single', postId
30 | ]);
31 | });
32 |
33 | describe('while subscribing', () => {
34 | it('should call just onData()', () => {
35 | const Meteor = {subscribe: stub()};
36 | const Collections = getCollections();
37 |
38 | const context = () => ({Meteor, Tracker, Collections});
39 | const postId = 'dwd';
40 | const onData = spy();
41 |
42 | composer({context, postId}, onData);
43 | expect(onData.args[0]).to.deep.equal([]);
44 | });
45 | });
46 |
47 | describe('after subscribed', () => {
48 | it('should find the post and send it to onData', () => {
49 | const Meteor = {subscribe: stub()};
50 |
51 | const post = {_id: 'post'};
52 | const Collections = getCollections(post);
53 |
54 | const context = () => ({Meteor, Collections, Tracker});
55 | const postId = 'dwd';
56 | const onData = spy();
57 |
58 | composer({context, postId}, onData);
59 | expect(onData.args[0]).to.deep.equal([ null, {post} ]);
60 | });
61 | });
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kickstart-mantrajs-webpack",
3 | "version": "1.0.0",
4 | "description": "Kickstart a project with Meteor, React and react-router fast!",
5 | "main": "src/server.js",
6 | "browser": "src/client.js",
7 | "scripts": {
8 | "lint": "eslint ./src/lib ./src/client ./src/server --ext .js --ext .jsx",
9 | "lintfix": "npm run lint -- --fix",
10 | "testonly": "mocha .scripts/mocha_boot.js src/client/**/tests/**/*.js --compilers js:babel-core/register",
11 | "test": "npm run lint && npm run testonly",
12 | "test-watch": "npm run testonly -- --watch --watch-extensions js,jsx"
13 | },
14 | "dependencies": {
15 | "domready": "^1.0.8",
16 | "mantra-core": "^1.2.0",
17 | "react": "^15.x.x",
18 | "react-addons-create-fragment": "^15.x.x",
19 | "react-addons-css-transition-group": "^15.x.x",
20 | "react-addons-linked-state-mixin": "^15.x.x",
21 | "react-addons-perf": "^15.x.x",
22 | "react-addons-pure-render-mixin": "^15.x.x",
23 | "react-addons-transition-group": "^15.x.x",
24 | "react-addons-update": "^15.x.x",
25 | "react-dom": "^15.x.x",
26 | "react-mounter": "^1.2.0"
27 | },
28 | "devDependencies": {
29 | "babel-core": "6.x.x",
30 | "babel-plugin-react-require": "2.x.x",
31 | "babel-polyfill": "6.x.x",
32 | "babel-loader": "^6.2.0",
33 | "babel-plugin-add-module-exports": "^0.1.2",
34 | "babel-plugin-react-transform": "^2.0.0",
35 | "babel-plugin-transform-decorators-legacy": "^1.3.2",
36 | "babel-preset-es2015": "^6.3.13",
37 | "babel-preset-react": "^6.3.13",
38 | "babel-preset-stage-0": "^6.3.13",
39 | "babel-root-slash-import": "1.x.x",
40 | "chai": "3.x.x",
41 | "css-loader": "^0.23.0",
42 | "enzyme": "^2.0.0",
43 | "eslint": "1.10.x",
44 | "eslint-plugin-react": "3.15.x",
45 | "extract-text-webpack-plugin": "^0.9.1",
46 | "file-loader": "^0.8.5",
47 | "jsdom": "^8.0.4",
48 | "jsx-control-statements": "^3.1.0",
49 | "mocha": "2.x.x",
50 | "react-addons-test-utils": "^15.x.x",
51 | "react-transform-catch-errors": "^1.0.0",
52 | "react-transform-hmr": "^1.0.1",
53 | "redbox-react": "^1.2.0",
54 | "sinon": "1.17.x",
55 | "style-loader": "^0.13.0",
56 | "url-loader": "^0.5.7",
57 | "webpack": "^1.13.0",
58 | "webpack-hot-middleware": "^2.4.1",
59 | "expose-loader": "^0.7.1",
60 | "babel": "^6.3.26",
61 | "json-loader": "^0.5.4"
62 | }
63 | }
--------------------------------------------------------------------------------
/src/client/modules/core/actions/tests/posts.js:
--------------------------------------------------------------------------------
1 | const { describe, it } = global;
2 | import {expect} from 'chai';
3 | import {spy, stub} from 'sinon';
4 | import actions from '../posts';
5 |
6 | describe('actions.posts', () => {
7 | describe('create', () => {
8 | it('should reject if title is not there', () => {
9 | const LocalState = {set: spy()};
10 | actions.create({LocalState}, null, 'content');
11 | const args = LocalState.set.args[0];
12 |
13 | expect(args[0]).to.be.equal('SAVING_ERROR');
14 | expect(args[1]).to.match(/required/);
15 | });
16 |
17 | it('should reject if content is not there', () => {
18 | const LocalState = {set: spy()};
19 | actions.create({LocalState}, 'title', null);
20 | const args = LocalState.set.args[0];
21 |
22 | expect(args[0]).to.be.equal('SAVING_ERROR');
23 | expect(args[1]).to.match(/required/);
24 | });
25 |
26 | it('should clear older LocalState for SAVING_ERROR', () => {
27 | const Meteor = {uuid: spy(), call: spy()};
28 | const LocalState = {set: spy()};
29 | const FlowRouter = {go: spy()};
30 |
31 | actions.create({LocalState, Meteor, FlowRouter}, 't', 'c');
32 | expect(LocalState.set.args[0]).to.deep.equal([ 'SAVING_ERROR', null ]);
33 | });
34 |
35 | it('should call Meteor.call to save the post', () => {
36 | const Meteor = {uuid: () => 'id', call: spy()};
37 | const LocalState = {set: spy()};
38 | const FlowRouter = {go: spy()};
39 |
40 | actions.create({LocalState, Meteor, FlowRouter}, 't', 'c');
41 | const methodArgs = Meteor.call.args[0];
42 |
43 | expect(methodArgs.slice(0, 4)).to.deep.equal([
44 | 'posts.create', 'id', 't', 'c'
45 | ]);
46 | expect(methodArgs[4]).to.be.a('function');
47 | });
48 |
49 | it('should redirect user to the post', () => {
50 | const id = 'dsds';
51 | const Meteor = {uuid: () => id, call: stub()};
52 | const LocalState = {set: spy()};
53 | const FlowRouter = {go: spy()};
54 | Meteor.call.callsArg(4);
55 |
56 | actions.create({Meteor, LocalState, FlowRouter}, 't', 'c');
57 | expect(FlowRouter.go.args[0][0]).to.be.equal(`/post/${id}`);
58 | });
59 |
60 | describe('after Meteor.call', () => {
61 | describe('if there is error', () => {
62 | it('should set SAVING_ERROR with the error message', () => {
63 | const Meteor = {uuid: () => 'id', call: stub()};
64 | const LocalState = {set: spy()};
65 | const FlowRouter = {go: spy()};
66 | const err = {message: 'Oops'};
67 | Meteor.call.callsArgWith(4, err);
68 |
69 | actions.create({Meteor, LocalState, FlowRouter}, 't', 'c');
70 | expect(LocalState.set.args[1]).to.deep.equal([ 'SAVING_ERROR', err.message ]);
71 | });
72 | });
73 | });
74 | });
75 |
76 | describe('clearErrors', () => {
77 | it('should clear SAVING_ERROR local state', () => {
78 | const LocalState = {set: spy()};
79 | actions.clearErrors({LocalState});
80 | expect(LocalState.set.callCount).to.be.equal(1);
81 | expect(LocalState.set.args[0]).to.deep.equal([ 'SAVING_ERROR', null ]);
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "browser": true,
5 | "node": true
6 | },
7 |
8 | "plugins": [
9 | "react"
10 | ],
11 |
12 | "ecmaFeatures": {
13 | "arrowFunctions": true,
14 | "binaryLiterals": true,
15 | "blockBindings": true,
16 | "classes": true,
17 | "defaultParams": true,
18 | "destructuring": true,
19 | "experimentalObjectRestSpread": true,
20 | "forOf": true,
21 | "generators": true,
22 | "globalReturn": true,
23 | "jsx": true,
24 | "modules": true,
25 | "objectLiteralComputedProperties": true,
26 | "objectLiteralDuplicateProperties": true,
27 | "objectLiteralShorthandMethods": true,
28 | "objectLiteralShorthandProperties": true,
29 | "octalLiterals": true,
30 | "regexUFlag": true,
31 | "regexYFlag": true,
32 | "restParams": true,
33 | "spread": true,
34 | "superInFunctions": true,
35 | "templateStrings": true,
36 | "unicodeCodePointEscapes": true
37 | },
38 |
39 | "rules": {
40 | "array-bracket-spacing": [2, "always"],
41 | "arrow-spacing": 2,
42 | "block-scoped-var": 0,
43 | "brace-style": [2, "1tbs", {"allowSingleLine": true}],
44 | "callback-return": 2,
45 | "camelcase": [2, {"properties": "always"}],
46 | "comma-dangle": 0,
47 | "comma-spacing": 0,
48 | "comma-style": [2, "last"],
49 | "complexity": 0,
50 | "computed-property-spacing": [2, "never"],
51 | "consistent-return": 0,
52 | "consistent-this": 0,
53 | "curly": [2, "all"],
54 | "default-case": 0,
55 | "dot-location": [2, "property"],
56 | "dot-notation": 0,
57 | "eol-last": 2,
58 | "eqeqeq": 2,
59 | "func-names": 0,
60 | "func-style": 0,
61 | "generator-star-spacing": [0, {"before": true, "after": false}],
62 | "guard-for-in": 2,
63 | "handle-callback-err": [2, "error"],
64 | "id-length": 0,
65 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"],
66 | "indent": [2, 2, {"SwitchCase": 1}],
67 | "init-declarations": 0,
68 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}],
69 | "linebreak-style": 2,
70 | "lines-around-comment": 0,
71 | "max-depth": 0,
72 | "max-len": [2, 100, 4],
73 | "max-nested-callbacks": 0,
74 | "max-params": 0,
75 | "max-statements": 0,
76 | "new-cap": 0,
77 | "new-parens": 2,
78 | "newline-after-var": 0,
79 | "no-array-constructor": 2,
80 | "no-bitwise": 0,
81 | "no-caller": 2,
82 | "no-catch-shadow": 0,
83 | "no-class-assign": 2,
84 | "no-cond-assign": 2,
85 | "no-console": 1,
86 | "no-const-assign": 2,
87 | "no-constant-condition": 2,
88 | "no-continue": 0,
89 | "no-control-regex": 0,
90 | "no-debugger": 1,
91 | "no-delete-var": 2,
92 | "no-div-regex": 2,
93 | "no-dupe-args": 2,
94 | "no-dupe-keys": 2,
95 | "no-duplicate-case": 2,
96 | "no-else-return": 2,
97 | "no-empty": 2,
98 | "no-empty-character-class": 2,
99 | "no-empty-label": 2,
100 | "no-eq-null": 0,
101 | "no-eval": 2,
102 | "no-ex-assign": 2,
103 | "no-extend-native": 2,
104 | "no-extra-bind": 2,
105 | "no-extra-boolean-cast": 2,
106 | "no-extra-parens": 0,
107 | "no-extra-semi": 2,
108 | "no-fallthrough": 2,
109 | "no-floating-decimal": 2,
110 | "no-func-assign": 2,
111 | "no-implicit-coercion": 2,
112 | "no-implied-eval": 2,
113 | "no-inline-comments": 0,
114 | "no-inner-declarations": [2, "functions"],
115 | "no-invalid-regexp": 2,
116 | "no-invalid-this": 0,
117 | "no-irregular-whitespace": 2,
118 | "no-iterator": 2,
119 | "no-label-var": 2,
120 | "no-labels": 0,
121 | "no-lone-blocks": 2,
122 | "no-lonely-if": 2,
123 | "no-loop-func": 0,
124 | "no-mixed-requires": [2, true],
125 | "no-mixed-spaces-and-tabs": 2,
126 | "no-multi-spaces": 2,
127 | "no-multi-str": 2,
128 | "no-multiple-empty-lines": 0,
129 | "no-native-reassign": 0,
130 | "no-negated-in-lhs": 2,
131 | "no-nested-ternary": 0,
132 | "no-new": 2,
133 | "no-new-func": 0,
134 | "no-new-object": 2,
135 | "no-new-require": 2,
136 | "no-new-wrappers": 2,
137 | "no-obj-calls": 2,
138 | "no-octal": 2,
139 | "no-octal-escape": 2,
140 | "no-param-reassign": 2,
141 | "no-path-concat": 2,
142 | "no-plusplus": 0,
143 | "no-process-env": 0,
144 | "no-process-exit": 0,
145 | "no-proto": 2,
146 | "no-redeclare": 2,
147 | "no-regex-spaces": 2,
148 | "no-restricted-modules": 0,
149 | "no-return-assign": 2,
150 | "no-script-url": 2,
151 | "no-self-compare": 0,
152 | "no-sequences": 2,
153 | "no-shadow": 2,
154 | "no-shadow-restricted-names": 2,
155 | "no-spaced-func": 2,
156 | "no-sparse-arrays": 2,
157 | "no-sync": 2,
158 | "no-ternary": 0,
159 | "no-this-before-super": 2,
160 | "no-throw-literal": 2,
161 | "no-trailing-spaces": 2,
162 | "no-undef": 2,
163 | "no-undef-init": 2,
164 | "no-undefined": 0,
165 | "no-underscore-dangle": 0,
166 | "no-unexpected-multiline": 2,
167 | "no-unneeded-ternary": 2,
168 | "no-unreachable": 2,
169 | "no-unused-expressions": 2,
170 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
171 | "no-use-before-define": 0,
172 | "no-useless-call": 2,
173 | "no-var": 0,
174 | "no-void": 2,
175 | "no-warning-comments": 0,
176 | "no-with": 2,
177 | "object-curly-spacing": [0, "always"],
178 | "object-shorthand": [2, "always"],
179 | "one-var": [2, "never"],
180 | "operator-assignment": [2, "always"],
181 | "operator-linebreak": [2, "after"],
182 | "padded-blocks": 0,
183 | "prefer-const": 0,
184 | "prefer-reflect": 0,
185 | "prefer-spread": 0,
186 | "quote-props": [2, "as-needed"],
187 | "quotes": [2, "single"],
188 | "radix": 2,
189 | "require-yield": 2,
190 | "semi": [2, "always"],
191 | "semi-spacing": [2, {"before": false, "after": true}],
192 | "sort-vars": 0,
193 | "space-after-keywords": [2, "always"],
194 | "space-before-blocks": [2, "always"],
195 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}],
196 | "space-in-parens": 0,
197 | "space-infix-ops": [2, {"int32Hint": false}],
198 | "space-return-throw-case": 2,
199 | "space-unary-ops": [2, {"words": true, "nonwords": false}],
200 | "spaced-comment": [2, "always"],
201 | "strict": 0,
202 | "use-isnan": 2,
203 | "valid-jsdoc": 0,
204 | "valid-typeof": 2,
205 | "vars-on-top": 0,
206 | "wrap-iife": 2,
207 | "wrap-regex": 0,
208 | "yoda": [2, "never", {"exceptRange": true}],
209 | "react/jsx-uses-react": 1
210 | }
211 | }
212 |
--------------------------------------------------------------------------------