├── .gitignore ├── LICENSE ├── README.md ├── bs-config.json ├── examples ├── fetch-data-from-api │ ├── LICENSE │ ├── README.md │ ├── controllers │ │ ├── api │ │ │ └── project-api-controller.ts │ │ ├── controller.ts │ │ └── home-controller.ts │ ├── karma.conf.js │ ├── models │ │ └── project.ts │ ├── mvc-service.ts │ ├── package.json │ ├── spec-finder.js │ ├── src │ │ ├── app │ │ │ ├── app.spec.tsx │ │ │ └── app.tsx │ │ ├── lib │ │ │ └── react-test-utils-extended.tsx │ │ ├── main.tsx │ │ ├── project │ │ │ ├── project-details.spec.tsx │ │ │ ├── project-details.tsx │ │ │ ├── project-list.spec.tsx │ │ │ └── project-list.tsx │ │ └── store │ │ │ ├── project-store.spec.tsx │ │ │ └── project-store.tsx │ ├── startup.ts │ ├── tsconfig.json │ ├── views │ │ └── home │ │ │ └── index.vash │ └── webpack.config.js └── redux-devtools-implementation │ ├── LICENSE │ ├── README.md │ ├── bs-config.json │ ├── karma.conf.js │ ├── package.json │ ├── spec-finder.js │ ├── src │ ├── app │ │ └── app-container.tsx │ ├── index.html │ ├── lib │ │ └── react-test-utils-extended.tsx │ ├── main.tsx │ ├── project │ │ ├── project-details.spec.tsx │ │ ├── project-details.tsx │ │ ├── project-list.spec.tsx │ │ └── project-list.tsx │ └── store │ │ ├── project-store-action-type.tsx │ │ ├── project-store-action.tsx │ │ ├── project-store.spec.tsx │ │ └── project-store.tsx │ ├── tsconfig.json │ └── webpack.config.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── spec-finder.js ├── src ├── app │ ├── app.spec.tsx │ └── app.tsx ├── index.html ├── lib │ └── react-test-utils-extended.tsx ├── main.tsx ├── project │ ├── project-details.spec.tsx │ ├── project-details.tsx │ ├── project-list.spec.tsx │ └── project-list.tsx └── store │ ├── project-store.spec.tsx │ └── project-store.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot 2 | node_modules 3 | npm-debug.log* 4 | *.js 5 | *.js.map 6 | 7 | !webpack.config.js 8 | !karma.conf.js 9 | !spec-finder.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-mobx-mobxstatetree 2 | ## What This Repo Is 3 | A simple application created in React with Mobx and Mobx-State-Tree for learning purpose. Its integration with Redux-DevTools can be found [here](https://github.com/samuelalvin/react-mobx-mobxstatetree/tree/master/examples/redux-devtools-implementation). Example of fetching data from an API can be found [here](https://github.com/samuelalvin/react-mobx-mobxstatetree/tree/master/examples/fetch-data-from-api). 4 | 5 | ## Installing This Repo 6 | Simply download and extract this repo and execute these commands in the terminal. 7 | ``` 8 | npm install 9 | npm start 10 | ``` 11 | Use `npm test` to tests the code using Karma and Jasmine. 12 | 13 | ## Opening an Issue 14 | Feel free to open an issue to give a feedback, critique, idea, advice, or anything to the author. 15 | 16 | ## App Notes 17 | ### Mobx-React 18 | *1. Injection* 19 | 20 | Use `Provider` and `inject` to inject a `props`' property to a react component without the need of passing that `props`' property to the parent components. 21 | 22 | ### Mobx-State-Tree 23 | *1. Using interface from Mobx-State-Tree model* 24 | 25 | Take `modelA` which is an mobx-state-tree model for example. 26 | ``` 27 | const ModelA = types.model("ModelA", { 28 | id: types.number 29 | }); 30 | ``` 31 | Code below shows how to get this `modelA` interface. 32 | ``` 33 | type IModelA = typeof ModelA.Type; 34 | ``` 35 | 36 | *2. Push a model to an array* 37 | 38 | One way to push a model to an array is using the model interface. 39 | ``` 40 | const ModelA = types.model("ModelA", { 41 | id: types.number 42 | }); 43 | 44 | type IModelA = typeof ModelA.Type; 45 | 46 | const ArrModelA = types.model("ArrModelA", { 47 | values: types.array(ModelA) 48 | }).action((self) => ({ 49 | add(id: number) { 50 | self.values.push({ 51 | id: id 52 | } as IModelA); 53 | } 54 | })); 55 | ``` 56 | 57 | *3. Handling model update on async action* 58 | 59 | When an action is async and its callback function update the model (example code shown below), a `Cannot modify '{the model}', the object is protected and can only be modified by using an action` error will be thrown. 60 | ``` 61 | const someModel = types.model("someModel", { 62 | name: types.string 63 | }).action((self) => ({ 64 | changeNameAsync() { 65 | someAsyncMethod().then(name => { 66 | self.name = name; // Error will be thrown because of this statement; 67 | }); 68 | } 69 | })); 70 | ``` 71 | This problem can be solved by using another action as the callback function. 72 | ``` 73 | const someModel = types.model("someModel", { 74 | name: types.string 75 | }).action((self) => ({ 76 | changeNameAsync() { 77 | someAsyncMethod().then((this as typeof someModel.Type).changeName); 78 | }, 79 | 80 | changeName(name) { 81 | self.name = name; 82 | } 83 | })); 84 | ``` 85 | 86 | 87 | ### Testing in Jasmine 88 | *1. Expecting an exception* 89 | 90 | Take `functionA` which throw an exception for example. 91 | ``` 92 | functionA() { 93 | throw new Error("some random error"); 94 | } 95 | ``` 96 | To catch `functionA`'s exception in Jasmine, use anonymous function. 97 | ``` 98 | expect(() => functionA()).toThrowError(); 99 | ``` 100 | -------------------------------------------------------------------------------- /bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "baseDir": "src", 4 | "routes": { 5 | "/node_modules": "node_modules" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/fetch-data-from-api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/fetch-data-from-api/README.md: -------------------------------------------------------------------------------- 1 | # fetch-data-from-api 2 | This example app consists of a backend and a frontend. The backend uses Express as its framework and the startup file is located in the `root` folder. The frontend is located in the `src` folder. 3 | ## Installing This App 4 | Execute these commands in the terminal. 5 | ``` 6 | npm install 7 | npm start 8 | ``` 9 | Use `npm test` to tests the code using Karma and Jasmine. 10 | -------------------------------------------------------------------------------- /examples/fetch-data-from-api/controllers/api/project-api-controller.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { Controller } from "../controller"; 3 | import { IProject } from "../../models/project"; 4 | 5 | export class ProjectApiController extends Controller { 6 | setupRoute(): express.Router { 7 | this.router.get("/", this.getProjects); 8 | return this.router; 9 | } 10 | 11 | private getProjects(request: express.Request, response: express.Response): void { 12 | const mockProjects: IProject[] = [{ 13 | id: 0, 14 | name: "debugProject1", 15 | isActive: true 16 | }, { 17 | id: 1, 18 | name: "debugProject2", 19 | isActive: false 20 | }, { 21 | id: 2, 22 | name: "debugProject3", 23 | isActive: false 24 | }, { 25 | id: 3, 26 | name: "debugProject4", 27 | isActive: true 28 | }, ]; 29 | 30 | response.send(mockProjects); 31 | } 32 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/controllers/controller.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | 3 | export abstract class Controller { 4 | protected router: express.Router; 5 | 6 | constructor() { 7 | this.router = express.Router(); 8 | } 9 | 10 | abstract setupRoute(): express.Router; 11 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/controllers/home-controller.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import { Controller } from "./controller"; 3 | 4 | export class HomeController extends Controller { 5 | setupRoute(): express.Router { 6 | this.router.get("/", (request, response) => { 7 | response.render("home/index"); 8 | }); 9 | 10 | return this.router; 11 | } 12 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require("./webpack.config.js"); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: "", 6 | frameworks: [ 7 | "jasmine" 8 | ], 9 | plugins: [ 10 | require("karma-jasmine"), 11 | require("karma-chrome-launcher"), 12 | require("karma-webpack") 13 | ], 14 | 15 | files: [ 16 | "spec-finder.js" 17 | ], 18 | 19 | preprocessors: { 20 | "spec-finder.js": ["webpack"] 21 | }, 22 | 23 | webpack: { 24 | module: webpackConfig.module, 25 | resolve: webpackConfig.resolve 26 | }, 27 | 28 | webpackMiddleware: { 29 | noInfo: true 30 | }, 31 | 32 | port: 9876, 33 | colors: true, 34 | logLevel: config.LOG_INFO, 35 | autoWatch: true, 36 | browsers: [ 37 | "ChromeHeadless" 38 | ], 39 | singleRun: true 40 | }) 41 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/models/project.ts: -------------------------------------------------------------------------------- 1 | export interface IProject { 2 | id: number, 3 | name: string, 4 | isActive: boolean 5 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/mvc-service.ts: -------------------------------------------------------------------------------- 1 | import { Express } from "express"; 2 | import * as path from "path"; 3 | import "vash"; 4 | 5 | import { HomeController } from "./controllers/home-controller"; 6 | 7 | import { ProjectApiController } from "./controllers/api/project-api-controller"; 8 | 9 | export class MvcService { 10 | constructor(private app: Express) { } 11 | 12 | addMVC(): void { 13 | this.initializeViews(); 14 | this.initializeControllers(); 15 | this.initializeApiControllers(); 16 | } 17 | 18 | private initializeViews(): void { 19 | this.app.set("view engine", "vash"); 20 | this.app.set("views", path.join(__dirname, "views")); 21 | } 22 | 23 | private initializeControllers(): void { 24 | this.app.use("/", new HomeController().setupRoute()); 25 | } 26 | 27 | private initializeApiControllers(): void { 28 | this.app.use("/api/projects", new ProjectApiController().setupRoute()); 29 | } 30 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-data-from-api", 3 | "version": "1.0.0", 4 | "description": "Fetch data from API to a Mobx-State-Tree store.", 5 | "scripts": { 6 | "build": "node ./node_modules/webpack/bin/webpack.js && tsc", 7 | "prestart": "npm run build", 8 | "start": "nodemon ./startup.js", 9 | "test": "karma start karma.conf.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/samuelalvin/react-mobx-mobxstatetree/tree/master/examples/fetch-data-from-api" 14 | }, 15 | "keywords": [ 16 | "reactjs", 17 | "mobx", 18 | "mobx-react", 19 | "mobx-state-tree", 20 | "expressjs", 21 | "mvc", 22 | "typescript" 23 | ], 24 | "author": "Samuel Hutama", 25 | "license": "MIT", 26 | "dependencies": { 27 | "express": "^4.15.4", 28 | "isomorphic-fetch": "^2.2.1", 29 | "mobx": "^3.2.2", 30 | "mobx-react": "^4.2.2", 31 | "mobx-state-tree": "^0.10.2", 32 | "react": "^15.6.1", 33 | "react-dom": "^15.6.1", 34 | "react-test-renderer": "^15.6.1", 35 | "vash": "^0.12.2" 36 | }, 37 | "devDependencies": { 38 | "@types/express": "^4.0.37", 39 | "@types/isomorphic-fetch": "0.0.34", 40 | "@types/jasmine": "^2.5.54", 41 | "@types/node": "^8.0.28", 42 | "@types/react": "^16.0.3", 43 | "@types/react-dom": "^15.5.4", 44 | "@types/react-test-renderer": "^15.5.4", 45 | "jasmine-core": "^2.7.0", 46 | "karma": "^1.7.0", 47 | "karma-chrome-launcher": "^2.2.0", 48 | "karma-jasmine": "^1.1.0", 49 | "karma-webpack": "^2.0.4", 50 | "nodemon": "^1.12.1", 51 | "ts-loader": "^2.3.3", 52 | "typescript": "^2.4.2", 53 | "typescript-compiler": "^1.4.1-2", 54 | "webpack": "^3.5.5" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/fetch-data-from-api/spec-finder.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./src', true, /.+\.spec\.tsx?$/); 2 | context.keys().forEach(context); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/app/app.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 3 | import App from "./app"; 4 | 5 | describe("app", function () { 6 | it("should be rendered without any problem", function () { 7 | let renderer = ReactShallowRenderer.createRenderer(); 8 | renderer.render(); 9 | 10 | let result = renderer.getRenderOutput(); 11 | expect(result).toBeDefined(); 12 | expect(result.type).toMatch("div"); 13 | }); 14 | }); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ProjectList from "../project/project-list"; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 |

Project List

9 | 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default App; -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/lib/react-test-utils-extended.tsx: -------------------------------------------------------------------------------- 1 | import * as TestUtils from "react-dom/test-utils"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | type ITestUtils = typeof TestUtils; 5 | 6 | interface ITestUtilsExtended extends ITestUtils { 7 | /** 8 | * Finds a DOM element of components in the rendered tree that are 9 | * DOM components with the DOM element name matching `elementName`. Returns undefined if no element matches `elementName`. 10 | */ 11 | findRenderedDOMComponentsWithName?(tree: React.Component, elementName: string): React.ReactInstance; 12 | 13 | /** 14 | * Finds all DOM elements of components in the rendered tree that are 15 | * DOM components with the DOM element name matching `elementName`. 16 | */ 17 | scryRenderedDOMComponentsWithName?(tree: React.Component, elementName: string): React.ReactInstance[]; 18 | } 19 | 20 | var TestUtilsExtended = TestUtils as ITestUtilsExtended; 21 | 22 | TestUtilsExtended.findRenderedDOMComponentsWithName = function (tree, elementName) { 23 | return TestUtils.findAllInRenderedTree(tree, function (inst) { 24 | return TestUtils.isDOMComponent(inst) && inst.getAttribute("name") == elementName; 25 | })[0]; 26 | } 27 | 28 | TestUtilsExtended.scryRenderedDOMComponentsWithName = function (tree, elementName) { 29 | return TestUtils.findAllInRenderedTree(tree, function (inst) { 30 | return TestUtils.isDOMComponent(inst) && inst.getAttribute("name") == elementName; 31 | }); 32 | } 33 | 34 | export default TestUtilsExtended; -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import App from "./app/app" 4 | import { Provider } from "mobx-react"; 5 | 6 | import projectStore, { Project } from "./store/project-store"; 7 | 8 | const mainApp = ( 9 | 10 | 11 | 12 | ); 13 | 14 | ReactDOM.render( 15 | mainApp, 16 | document.getElementById("app") 17 | ); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/project/project-details.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TestUtils from "react-dom/test-utils"; 3 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 4 | import { Project } from "../store/project-store"; 5 | import ProjectDetails, { IProjectListProps } from "./project-details"; 6 | 7 | const project = Project.create({ 8 | id: 0, 9 | name: "debugProject1" 10 | }); 11 | 12 | describe("project-details", function () { 13 | let deleteProject: jasmine.Spy; 14 | 15 | beforeEach(function() { 16 | deleteProject = jasmine.createSpy("deleteProject"); 17 | }); 18 | 19 | it("should be created without any problem", function () { 20 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 21 | expect(projectDetails).toBeDefined(); 22 | }); 23 | 24 | it("should be rendered without any problem", function () { 25 | let renderer = ReactShallowRenderer.createRenderer(); 26 | renderer.render(); 27 | 28 | let result = renderer.getRenderOutput(); 29 | expect(result).toBeDefined(); 30 | expect(result.type).toMatch("li"); 31 | expect((result.props.children[0] as React.ReactElement).type).toMatch("span"); 32 | }); 33 | 34 | it("should show span on nonEditMode", function () { 35 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 36 | let span = TestUtils.findRenderedDOMComponentWithTag(projectDetails, "span"); 37 | let inputs = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "input"); 38 | 39 | expect(span).toBeDefined(); 40 | expect(inputs.length).toEqual(0); 41 | }); 42 | 43 | it("should show input on editMode", function () { 44 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 45 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "button"); 46 | let editButton = buttons[0]; 47 | 48 | TestUtils.Simulate.click(buttons[0]); 49 | 50 | let inputs = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "input"); 51 | expect(inputs.length).toBeGreaterThan(0); 52 | }); 53 | 54 | it("should be able to delete a project", function () { 55 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 56 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "button"); 57 | let deleteButton = buttons[1]; 58 | 59 | TestUtils.Simulate.click(deleteButton); 60 | expect(deleteProject).toHaveBeenCalledTimes(1); 61 | }); 62 | }); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/project/project-details.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { observable } from "mobx"; 4 | 5 | import { IProject } from "../store/project-store"; 6 | 7 | export interface IProjectListProps { 8 | project: IProject 9 | onDeletion(id: number): void; 10 | } 11 | 12 | @observer 13 | class ProjectDetails extends React.Component { 14 | @observable private editMode: boolean = false; 15 | 16 | constructor(props){ 17 | super(props); 18 | this.toggleEditMode = this.toggleEditMode.bind(this); 19 | this.getProjectForm = this.getProjectForm.bind(this); 20 | } 21 | 22 | toggleEditMode(): void { 23 | this.editMode = !this.editMode; 24 | } 25 | 26 | getProjectForm(project: IProject): JSX.Element { 27 | if (this.editMode) { 28 | return ( 29 |
30 | 31 | 32 |
33 | ); 34 | } 35 | 36 | return ( 37 | {project.name} - {project.isActive ? "active" : "inactive"} 38 | ); 39 | } 40 | 41 | render(): JSX.Element { 42 | const { project } = this.props; 43 | return ( 44 |
  • 45 | {this.getProjectForm(project)} 46 | 47 | 48 |
  • 49 | ); 50 | } 51 | } 52 | 53 | export default ProjectDetails; -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/project/project-list.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 3 | import ProjectList, { IProjectListProps } from "./project-list"; 4 | import { ProjectStore, IProjectStore, IProject } from "../store/project-store"; 5 | import TestUtilsExtended from "../lib/react-test-utils-extended"; 6 | 7 | function initializeProjectStore(): IProjectStore { 8 | return ProjectStore.create({ 9 | projects: [{ 10 | id: 0, 11 | name: "debugProject1", 12 | isActive: true 13 | } as IProject] 14 | }); 15 | }; 16 | 17 | describe("project-list", function () { 18 | it("should be created without any problem", function () { 19 | let projectStore = initializeProjectStore(); 20 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 21 | expect(projectList).toBeDefined(); 22 | }); 23 | 24 | it("should display projects", function(){ 25 | let projectStore = initializeProjectStore(); 26 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 27 | let projects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 28 | expect(projects.length).toEqual(projectStore.projects.length); 29 | }); 30 | 31 | it("should be able to add a project", function() { 32 | let projectStore = initializeProjectStore(); 33 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 34 | let nameInput = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "newProjectNameInput"); 35 | let checkBoxInput = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "newProjectStatusInput"); 36 | let addButton = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "addProjectButton"); 37 | let projects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 38 | expect(projects.length).toEqual(1); 39 | 40 | TestUtilsExtended.Simulate.change(nameInput, { 41 | target: { 42 | value : "debugProject2" 43 | } 44 | } as any); 45 | TestUtilsExtended.Simulate.click(addButton); 46 | 47 | projects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 48 | expect(projects.length).toEqual(2); 49 | }); 50 | }); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/project/project-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, inject } from "mobx-react"; 3 | 4 | import { IProjectStore, Project, IProject } from "../store/project-store"; 5 | import ProjectDetails from "./project-details"; 6 | 7 | export interface IProjectListProps { 8 | projectStore?: IProjectStore 9 | } 10 | 11 | @inject("projectStore") 12 | @observer 13 | class ProjectList extends React.Component { 14 | private newProject: IProject; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.newProject = Project.create({ 20 | id: -1, 21 | name: "" 22 | }); 23 | 24 | this.deleteProject = this.deleteProject.bind(this); 25 | this.addProject = this.addProject.bind(this); 26 | 27 | const { projectStore } = props; 28 | projectStore.getProjects(); 29 | } 30 | 31 | addProject(newProject: IProject): void { 32 | const { projectStore } = this.props; 33 | projectStore.addProject(newProject); 34 | } 35 | 36 | deleteProject(id: number): void { 37 | const { projectStore } = this.props; 38 | projectStore.deleteProject(id); 39 | } 40 | 41 | render() { 42 | const { projectStore } = this.props; 43 | return ( 44 |
    45 |
      46 | {projectStore.projects.map((project) => ())} 47 |
    48 |
    49 | 50 | 51 |
    52 | 53 |
    54 | ); 55 | } 56 | } 57 | 58 | export default ProjectList; -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/store/project-store.spec.tsx: -------------------------------------------------------------------------------- 1 | import ProjectList from "../project/project-list"; 2 | import { ProjectStore, IProjectStore, IProject } from "../store/project-store"; 3 | 4 | function initializeProjectStore(): IProjectStore { 5 | return ProjectStore.create({ 6 | projects: [{ 7 | id: 0, 8 | name: "debugProject1", 9 | isActive: true 10 | } as IProject] 11 | }); 12 | }; 13 | 14 | describe("project-store", function () { 15 | it("should be initialized without any problem", function () { 16 | let projectStore = initializeProjectStore(); 17 | expect(projectStore).toBeDefined(); 18 | expect(projectStore.projects.length).toEqual(1); 19 | expect(projectStore.projects[0].name).toMatch("debugProject1"); 20 | }); 21 | 22 | it("should be able to add project", function () { 23 | let projectStore = initializeProjectStore(); 24 | let newProject = { 25 | name: "debugProject2", 26 | isActive: true 27 | } as IProject; 28 | 29 | projectStore.addProject(newProject); 30 | expect(projectStore.projects.length).toEqual(2); 31 | expect(projectStore.projects[1].name).toMatch("debugProject2"); 32 | expect(projectStore.projects[1].isActive).toBeTruthy(); 33 | }); 34 | 35 | it("should throw error on adding project with empty name", function () { 36 | let projectStore = initializeProjectStore(); 37 | let newProject = { 38 | name: "", 39 | isActive: true 40 | } as IProject; 41 | 42 | expect(() => projectStore.addProject(newProject)).toThrowError(); 43 | }); 44 | 45 | it("should be able to delete project", function () { 46 | let projectStore = initializeProjectStore(); 47 | projectStore.deleteProject(projectStore.projects[0].id); 48 | expect(projectStore.projects.length).toEqual(0); 49 | }); 50 | 51 | it("should throw error on deleting nonexistant project", function () { 52 | let projectStore = initializeProjectStore(); 53 | expect(() => projectStore.deleteProject(99)).toThrowError(); 54 | }); 55 | 56 | it("should be able to get unique id", function() { 57 | let projectStore = initializeProjectStore(); 58 | let newProject = { 59 | name: "debugProject2", 60 | isActive: true 61 | } as IProject; 62 | 63 | projectStore.addProject(newProject); 64 | expect(projectStore.projects[0].id).toEqual(0); 65 | expect(projectStore.projects[1].id).toEqual(1); 66 | 67 | projectStore.deleteProject(projectStore.projects[1].id); 68 | projectStore.addProject(newProject); 69 | expect(projectStore.projects[0].id).toEqual(0); 70 | expect(projectStore.projects[1].id).toEqual(1); 71 | 72 | projectStore.deleteProject(projectStore.projects[0].id); 73 | projectStore.addProject(newProject); 74 | expect(projectStore.projects[0].id).toEqual(1); 75 | expect(projectStore.projects[1].id).toEqual(0); 76 | }); 77 | 78 | it("should be able to change project name", function() { 79 | let projectStore = initializeProjectStore(); 80 | projectStore.projects[0].changeName("debugProject2"); 81 | expect(projectStore.projects[0].name).toEqual("debugProject2"); 82 | }); 83 | 84 | it("should throw error on changing project name to empty string", function() { 85 | let projectStore = initializeProjectStore(); 86 | expect(() => projectStore.projects[0].changeName("")).toThrowError(); 87 | }); 88 | 89 | it("should be able to toggle project status", function() { 90 | let projectStore = initializeProjectStore(); 91 | projectStore.projects[0].toggleActive(); 92 | expect(projectStore.projects[0].isActive).toBeFalsy(); 93 | 94 | projectStore.projects[0].toggleActive(); 95 | expect(projectStore.projects[0].isActive).toBeTruthy(); 96 | }); 97 | }); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/src/store/project-store.tsx: -------------------------------------------------------------------------------- 1 | import { IObservableArray } from "mobx"; 2 | import { types } from "mobx-state-tree"; 3 | import * as fetch from "isomorphic-fetch"; 4 | 5 | export const Project = types.model("Project", { 6 | id: types.number, 7 | name: types.string, 8 | isActive: types.optional(types.boolean, false) 9 | }).actions((self) => ({ 10 | changeName(newName: string): void { 11 | if (!newName || newName.length == 0) { 12 | throw new Error("Project Model Action Error: new name should not be empty"); 13 | } 14 | 15 | self.name = newName; 16 | }, 17 | 18 | toggleActive(): void { 19 | self.isActive = !self.isActive; 20 | } 21 | })); 22 | 23 | export const ProjectStore = types.model("ProjectStore", { 24 | projects: types.array(Project) 25 | }).actions((self) => ({ 26 | async getProjects(): Promise> { 27 | let response = await fetch("/api/projects"); 28 | let projects = await response.json() as IObservableArray; 29 | return (self as IProjectStore).loadProjects(projects); 30 | }, 31 | 32 | loadProjects(projects: IObservableArray): IObservableArray { 33 | self.projects = projects; 34 | return self.projects; 35 | }, 36 | 37 | addProject(newProject: IProject): void { 38 | if (!newProject.name || newProject.name.length == 0) { 39 | throw new Error("ProjectStore Model Action Error: new project name should not be empty"); 40 | } 41 | 42 | let id = getUniqueProjectId(self.projects); 43 | self.projects.push({ 44 | id: id, 45 | name: newProject.name, 46 | isActive: newProject.isActive 47 | } as IProject); 48 | }, 49 | 50 | deleteProject(id: number): void { 51 | let index = self.projects.findIndex(project => project.id == id); 52 | if (index == -1) { 53 | throw new Error("ProjectStore Model Action Error: project not found"); 54 | } 55 | 56 | self.projects.splice(index, 1); 57 | } 58 | })); 59 | 60 | function getUniqueProjectId (projects: IObservableArray): number { 61 | let id = 0; 62 | projects.map(project => project.id).forEach((currentId, currentIndex) => { 63 | if (currentId != currentIndex) { 64 | id = currentIndex; 65 | } 66 | }); 67 | 68 | if (projects.find(project => project.id == id)) { 69 | id = projects.length; 70 | } 71 | 72 | return id; 73 | }; 74 | 75 | export type IProject = typeof Project.Type; 76 | 77 | export type IProjectStore = typeof ProjectStore.Type; 78 | 79 | const projectStore = ProjectStore.create({ 80 | projects: [{ 81 | id: 0, 82 | name: "debugProject1", 83 | isActive: true 84 | } as IProject] 85 | }); 86 | 87 | export default projectStore; -------------------------------------------------------------------------------- /examples/fetch-data-from-api/startup.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import * as express from "express"; 3 | import { MvcService } from "./mvc-service"; 4 | 5 | class Startup { 6 | readonly app: express.Express; 7 | 8 | constructor() { 9 | this.app = express(); 10 | this.configureServices(); 11 | this.configureApp(); 12 | } 13 | 14 | private configureServices() { 15 | const mvcService = new MvcService(this.app); 16 | mvcService.addMVC(); 17 | } 18 | 19 | private configureApp() { 20 | this.app.use(express.static("wwwroot")); 21 | } 22 | } 23 | 24 | const startup = new Startup(); 25 | const server = http.createServer(startup.app); 26 | server.listen(3000); -------------------------------------------------------------------------------- /examples/fetch-data-from-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "experimentalDecorators": true, 5 | "noEmitOnError": false, 6 | "removeComments": true, 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "target": "es5", 10 | "types": [ 11 | "@types/express", 12 | "@types/isomorphic-fetch", 13 | "@types/node", 14 | "@types/react", 15 | "@types/react-dom", 16 | "@types/react-test-renderer", 17 | "@types/jasmine" 18 | ], 19 | "lib": [ 20 | "es2015", 21 | "dom" 22 | ] 23 | }, 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /examples/fetch-data-from-api/views/home/index.vash: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /examples/fetch-data-from-api/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/main.tsx", 3 | 4 | output: { 5 | filename: "bundle.js", 6 | path: __dirname + "/wwwroot", 7 | }, 8 | 9 | resolve: { 10 | extensions: [".tsx", ".js"] 11 | }, 12 | 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.tsx?$/, 17 | exclude: /node_modules/, 18 | loaders: ["ts-loader"] 19 | } 20 | ] 21 | } 22 | }; -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/README.md: -------------------------------------------------------------------------------- 1 | # redux-devtools-implementation 2 | ## Installing This App 3 | Execute these commands in the terminal. 4 | ``` 5 | npm install 6 | npm start 7 | ``` 8 | Use `npm test` to tests the code using Karma and Jasmine. 9 | ## Using Redux-DevTools 10 | The app is integrated with Redux-DevTools. Install Redux-DevTools from [here](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) for Chrome, or [here](https://addons.mozilla.org/en-US/firefox/addon/remotedev/) for Firefox. 11 | -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "baseDir": "src", 4 | "routes": { 5 | "/node_modules": "node_modules" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require("./webpack.config.js"); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: "", 6 | frameworks: [ 7 | "jasmine" 8 | ], 9 | plugins: [ 10 | require("karma-jasmine"), 11 | require("karma-chrome-launcher"), 12 | require("karma-webpack") 13 | ], 14 | 15 | files: [ 16 | "spec-finder.js" 17 | ], 18 | 19 | preprocessors: { 20 | "spec-finder.js": ["webpack"] 21 | }, 22 | 23 | webpack: { 24 | module: webpackConfig.module, 25 | resolve: webpackConfig.resolve 26 | }, 27 | 28 | webpackMiddleware: { 29 | noInfo: true 30 | }, 31 | 32 | port: 9876, 33 | colors: true, 34 | logLevel: config.LOG_INFO, 35 | autoWatch: true, 36 | browsers: [ 37 | "ChromeHeadless" 38 | ], 39 | singleRun: true 40 | }) 41 | } -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-devtools-implementation", 3 | "version": "1.0.0", 4 | "description": "A Mobx-State-Tree store integrated with Redux-DevTools.", 5 | "scripts": { 6 | "build": "node ./node_modules/webpack/bin/webpack.js", 7 | "serve": "lite-server -c=bs-config.json", 8 | "prestart": "npm run build", 9 | "start": "npm run build | npm run serve", 10 | "test": "karma start karma.conf.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/samuelalvin/react-mobx-mobxstatetree/tree/master/examples/redux-devtools-implementation" 15 | }, 16 | "keywords": [ 17 | "reactjs", 18 | "mobx", 19 | "mobx-react", 20 | "mobx-state-tree", 21 | "redux-devtools", 22 | "typescript" 23 | ], 24 | "author": "Samuel Hutama", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@types/react": "^16.0.3", 28 | "@types/react-dom": "^15.5.4", 29 | "@types/react-test-renderer": "^15.5.4", 30 | "mobx": "^3.2.2", 31 | "mobx-react": "^4.2.2", 32 | "mobx-state-tree": "^0.10.2", 33 | "react": "^15.6.1", 34 | "react-dom": "^15.6.1", 35 | "react-redux": "^5.0.6", 36 | "react-test-renderer": "^15.6.1", 37 | "redux": "^3.7.2", 38 | "remotedev": "^0.2.7" 39 | }, 40 | "devDependencies": { 41 | "@types/jasmine": "^2.5.54", 42 | "jasmine-core": "^2.7.0", 43 | "karma": "^1.7.0", 44 | "karma-chrome-launcher": "^2.2.0", 45 | "karma-jasmine": "^1.1.0", 46 | "karma-webpack": "^2.0.4", 47 | "lite-server": "^2.3.0", 48 | "ts-loader": "^2.3.3", 49 | "typescript": "^2.4.2", 50 | "typescript-compiler": "^1.4.1-2", 51 | "webpack": "^3.5.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/spec-finder.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./src', true, /.+\.spec\.tsx?$/); 2 | context.keys().forEach(context); -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/app/app-container.tsx: -------------------------------------------------------------------------------- 1 | // TODO: Refactor app-container 2 | 3 | import * as React from "react"; 4 | import * as PropTypes from "prop-types"; 5 | import { bindActionCreators } from "redux"; 6 | import { connect } from "react-redux"; 7 | import ProjectList from "../project/project-list"; 8 | import { addProject, changeName, deleteProject, toggleActive, IProjectStoreAction } from "../store/project-store-action"; 9 | import { IProject } from "../store/project-store"; 10 | import { IObservableArray } from "mobx"; 11 | 12 | interface IAppProps { 13 | projects: IObservableArray, 14 | actions: IProjectStoreAction 15 | } 16 | 17 | class AppContainer extends React.Component { 18 | static propTypes = { 19 | projects: PropTypes.array.isRequired, 20 | actions: PropTypes.object.isRequired 21 | } 22 | 23 | render() { 24 | return ( 25 |
    26 |

    Project List

    27 | 28 |
    29 | ); 30 | } 31 | } 32 | 33 | const mapStateToProps = state => ({ 34 | projects: state.projects 35 | }) 36 | 37 | const ProjectStoreActions = { 38 | addProject, 39 | changeName, 40 | deleteProject, 41 | toggleActive 42 | } 43 | 44 | const mapDispatchToProps = dispatch => ({ 45 | actions: bindActionCreators(ProjectStoreActions, dispatch) 46 | }) 47 | 48 | export default connect( 49 | mapStateToProps, 50 | mapDispatchToProps 51 | )(AppContainer) -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/index.html: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/lib/react-test-utils-extended.tsx: -------------------------------------------------------------------------------- 1 | import * as TestUtils from "react-dom/test-utils"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | type ITestUtils = typeof TestUtils; 5 | 6 | interface ITestUtilsExtended extends ITestUtils { 7 | /** 8 | * Finds a DOM element of components in the rendered tree that are 9 | * DOM components with the DOM element name matching `elementName`. Returns undefined if no element matches `elementName`. 10 | */ 11 | findRenderedDOMComponentsWithName?(tree: React.Component, elementName: string): React.ReactInstance; 12 | 13 | /** 14 | * Finds all DOM elements of components in the rendered tree that are 15 | * DOM components with the DOM element name matching `elementName`. 16 | */ 17 | scryRenderedDOMComponentsWithName?(tree: React.Component, elementName: string): React.ReactInstance[]; 18 | } 19 | 20 | var TestUtilsExtended = TestUtils as ITestUtilsExtended; 21 | 22 | TestUtilsExtended.findRenderedDOMComponentsWithName = function (tree, elementName) { 23 | return TestUtils.findAllInRenderedTree(tree, function (inst) { 24 | return TestUtils.isDOMComponent(inst) && inst.getAttribute("name") == elementName; 25 | })[0]; 26 | } 27 | 28 | TestUtilsExtended.scryRenderedDOMComponentsWithName = function (tree, elementName) { 29 | return TestUtils.findAllInRenderedTree(tree, function (inst) { 30 | return TestUtils.isDOMComponent(inst) && inst.getAttribute("name") == elementName; 31 | }); 32 | } 33 | 34 | export default TestUtilsExtended; -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import * as RemoteDev from "remotedev"; 5 | import { asReduxStore, connectReduxDevtools } from "mobx-state-tree"; 6 | import AppContainer from "./app/app-container"; 7 | import projectStore, { Project } from "./store/project-store"; 8 | 9 | const store = asReduxStore(projectStore); 10 | connectReduxDevtools(RemoteDev, projectStore); 11 | 12 | const mainApp = ( 13 | 14 | 15 | 16 | ); 17 | 18 | ReactDOM.render( 19 | mainApp, 20 | document.getElementById("app") 21 | ); -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/project/project-details.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TestUtils from "react-dom/test-utils"; 3 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 4 | import { Project } from "../store/project-store"; 5 | import ProjectDetails, { IProjectListProps } from "./project-details"; 6 | 7 | const project = Project.create({ 8 | id: 0, 9 | name: "debugProject1" 10 | }); 11 | 12 | describe("project-details", function () { 13 | let changeName: jasmine.Spy; 14 | let toggleActive: jasmine.Spy; 15 | let deleteProject: jasmine.Spy; 16 | 17 | beforeEach(function() { 18 | changeName = jasmine.createSpy("changeName"); 19 | toggleActive = jasmine.createSpy("toggleActive"); 20 | deleteProject = jasmine.createSpy("deleteProject"); 21 | }); 22 | 23 | it("should be created without any problem", function () { 24 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 25 | expect(projectDetails).toBeDefined(); 26 | }); 27 | 28 | it("should be rendered without any problem", function () { 29 | let renderer = ReactShallowRenderer.createRenderer(); 30 | renderer.render(); 31 | 32 | let result = renderer.getRenderOutput(); 33 | expect(result).toBeDefined(); 34 | expect(result.type).toMatch("li"); 35 | expect((result.props.children[0] as React.ReactElement).type).toMatch("span"); 36 | }); 37 | 38 | it("should show span on nonEditMode", function () { 39 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 40 | let span = TestUtils.findRenderedDOMComponentWithTag(projectDetails, "span"); 41 | let inputs = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "input"); 42 | 43 | expect(span).toBeDefined(); 44 | expect(inputs.length).toEqual(0); 45 | }); 46 | 47 | it("should show input on editMode", function () { 48 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 49 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "button"); 50 | let editButton = buttons[0]; 51 | 52 | TestUtils.Simulate.click(buttons[0]); 53 | 54 | let inputs = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "input"); 55 | expect(inputs.length).toBeGreaterThan(0); 56 | }); 57 | 58 | it("should be able to delete a project", function () { 59 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 60 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "button"); 61 | let deleteButton = buttons[1]; 62 | 63 | TestUtils.Simulate.click(deleteButton); 64 | expect(deleteProject).toHaveBeenCalledTimes(1); 65 | }); 66 | }); -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/project/project-details.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { observable } from "mobx"; 4 | 5 | import { IProject } from "../store/project-store"; 6 | 7 | export interface IProjectListProps { 8 | project: IProject 9 | onChangeName(id: number, newName: string): void; 10 | onToggleActive(id: number): void; 11 | onDeletion(id: number): void; 12 | } 13 | 14 | @observer 15 | class ProjectDetails extends React.Component { 16 | @observable private editMode: boolean = false; 17 | 18 | constructor(props){ 19 | super(props); 20 | this.toggleEditMode = this.toggleEditMode.bind(this); 21 | this.getProjectForm = this.getProjectForm.bind(this); 22 | } 23 | 24 | toggleEditMode(): void { 25 | this.editMode = !this.editMode; 26 | } 27 | 28 | getProjectForm(project: IProject): JSX.Element { 29 | if (this.editMode) { 30 | return ( 31 |
    32 | 33 | 34 |
    35 | ); 36 | } 37 | 38 | return ( 39 | {project.name} - {project.isActive ? "active" : "inactive"} 40 | ); 41 | } 42 | 43 | render(): JSX.Element { 44 | const { project } = this.props; 45 | return ( 46 |
  • 47 | {this.getProjectForm(project)} 48 | 49 | 50 |
  • 51 | ); 52 | } 53 | } 54 | 55 | export default ProjectDetails; -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/project/project-list.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 3 | import { asReduxStore, getSnapshot } from "mobx-state-tree"; 4 | import { IObservableArray } from "mobx"; 5 | import ProjectList, { IProjectListProps } from "./project-list"; 6 | import { ProjectStore, IProject } from "../store/project-store"; 7 | import TestUtilsExtended from "../lib/react-test-utils-extended"; 8 | import { IProjectStoreAction } from "../store/project-store-action"; 9 | 10 | function getProjectsFromStore(): IObservableArray { 11 | let projectStore = ProjectStore.create({ 12 | projects: [{ 13 | id: 0, 14 | name: "debugProject1", 15 | isActive: true 16 | } as IProject] 17 | }); 18 | 19 | const store = asReduxStore(projectStore); 20 | return getSnapshot(projectStore).projects; 21 | }; 22 | 23 | const projectStoreActions = { 24 | addProject: jasmine.createSpy("addProject"), 25 | changeName: jasmine.createSpy("changeName"), 26 | deleteProject: jasmine.createSpy("deleteProject"), 27 | toggleActive: jasmine.createSpy("toggleActive") 28 | } 29 | 30 | describe("project-list", function () { 31 | it("should be created without any problem", function () { 32 | let projects = getProjectsFromStore(); 33 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 34 | expect(projectList).toBeDefined(); 35 | }); 36 | 37 | it("should display projects", function(){ 38 | let projects = getProjectsFromStore(); 39 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 40 | let displayedProjects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 41 | expect(displayedProjects.length).toEqual(projects.length); 42 | }); 43 | 44 | it("should be able to add a project", function() { 45 | let projects = getProjectsFromStore(); 46 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 47 | let addButton = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "addProjectButton"); 48 | TestUtilsExtended.Simulate.click(addButton); 49 | expect(projectStoreActions.addProject).toHaveBeenCalledTimes(1); 50 | }); 51 | }); -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/project/project-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, inject } from "mobx-react"; 3 | import { observable } from "mobx"; 4 | 5 | import { IProject } from "../store/project-store"; 6 | import { IProjectStoreAction } from "../store/project-store-action"; 7 | import ProjectDetails from "./project-details"; 8 | import { IObservableArray } from "mobx"; 9 | 10 | export interface IProjectListProps { 11 | projects: IObservableArray 12 | actions: IProjectStoreAction 13 | } 14 | 15 | @observer 16 | class ProjectList extends React.Component { 17 | @observable private newProject: IProject; 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | this.newProject = { 23 | id: -1, 24 | name: "", 25 | isActive: false 26 | } as IProject; 27 | 28 | this.deleteProject = this.deleteProject.bind(this); 29 | this.addProject = this.addProject.bind(this); 30 | this.changeName = this.changeName.bind(this); 31 | this.toggleActive = this.toggleActive.bind(this); 32 | } 33 | 34 | addProject(newProject: IProject): void { 35 | const { projects } = this.props; 36 | this.props.actions.addProject(newProject); 37 | } 38 | 39 | changeName(id: number, newName: string): void { 40 | const { projects } = this.props; 41 | this.props.actions.changeName(id, newName); 42 | } 43 | 44 | toggleActive(id: number): void { 45 | const { projects } = this.props; 46 | this.props.actions.toggleActive(id); 47 | } 48 | 49 | deleteProject(id: number): void { 50 | const { projects } = this.props; 51 | this.props.actions.deleteProject(id); 52 | } 53 | 54 | render() { 55 | const { projects } = this.props; 56 | return ( 57 |
    58 |
      59 | {projects.map((project) => ())} 60 |
    61 |
    62 | 63 | 64 |
    65 | 66 |
    67 | ); 68 | } 69 | } 70 | 71 | export default ProjectList; -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/store/project-store-action-type.tsx: -------------------------------------------------------------------------------- 1 | export const ADD_PROJECT = "ADD_PROJECT"; 2 | export const DELETE_PROJECT = "DELETE_PROJECT"; 3 | export const CHANGE_NAME = "CHANGE_NAME"; 4 | export const TOGGLE_ACTIVE = "TOGGLE_ACTIVE"; -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/store/project-store-action.tsx: -------------------------------------------------------------------------------- 1 | // TODO: Refactor project-store-action 2 | 3 | import * as types from "./project-store-action-type"; 4 | import { IProject } from "./project-store"; 5 | 6 | export const addProject = (newProject: IProject) => ({ type: types.ADD_PROJECT, newProject: newProject }); 7 | export const deleteProject = (id: number) => ({ type: types.DELETE_PROJECT, id }); 8 | export const changeName = (id: number, newName: string) => ({ type: types.CHANGE_NAME, id, newName }); 9 | export const toggleActive = (id: number) => ({ type: types.TOGGLE_ACTIVE, id }); 10 | 11 | export interface IProjectStoreAction { 12 | addProject: (newProject: IProject) => void, 13 | deleteProject: (id: number) => void, 14 | changeName: (id: number, newName: string) => void, 15 | toggleActive: (id: number) => void 16 | } -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/store/project-store.spec.tsx: -------------------------------------------------------------------------------- 1 | // TODO: refactor project-store.spec 2 | 3 | import { IObservableArray } from "mobx"; 4 | import { asReduxStore, getSnapshot } from "mobx-state-tree"; 5 | import * as types from "./project-store-action-type"; 6 | import ProjectList from "../project/project-list"; 7 | import { ProjectStore, IProject } from "../store/project-store"; 8 | 9 | function callProjectStoreAction(action): IObservableArray { 10 | let projectStore = ProjectStore.create({ 11 | projects: [{ 12 | id: 0, 13 | name: "debugProject1", 14 | isActive: true 15 | } as IProject] 16 | }); 17 | 18 | let store = asReduxStore(projectStore); 19 | if (action.type) { 20 | store.dispatch(action); 21 | } 22 | 23 | return getSnapshot(projectStore).projects; 24 | }; 25 | 26 | describe("project-store", function () { 27 | it("should be initialized without any problem", function () { 28 | let projects = callProjectStoreAction({}); 29 | expect(projects).toBeDefined(); 30 | expect(projects.length).toEqual(1); 31 | expect(projects[0].name).toMatch("debugProject1"); 32 | }); 33 | 34 | it("should be able to add project", function () { 35 | let newProject = { 36 | name: "debugProject2", 37 | isActive: true 38 | } as IProject; 39 | 40 | let projects = callProjectStoreAction( 41 | { type: types.ADD_PROJECT, newProject } 42 | ); 43 | 44 | expect(projects.length).toEqual(2); 45 | expect(projects[1].name).toMatch("debugProject2"); 46 | expect(projects[1].isActive).toBeTruthy(); 47 | }); 48 | 49 | it("should throw error on adding project with empty name", function () { 50 | let newProject = { 51 | name: "", 52 | isActive: true 53 | } as IProject; 54 | 55 | expect(() => callProjectStoreAction({ type: types.ADD_PROJECT, newProject })).toThrowError(); 56 | }); 57 | 58 | it("should be able to delete project", function () { 59 | let projects = callProjectStoreAction( 60 | { type: types.DELETE_PROJECT, id: 0 } 61 | ); 62 | 63 | expect(projects.length).toEqual(0); 64 | }); 65 | 66 | it("should throw error on deleting nonexistant project", function () { 67 | expect(() => callProjectStoreAction({ type: types.DELETE_PROJECT, id: 99 })).toThrowError(); 68 | }); 69 | 70 | // it("should be able to get unique id", function() { 71 | // let projectStore = initializeProjectStore(); 72 | // let newProject = { 73 | // name: "debugProject2", 74 | // isActive: true 75 | // } as IProject; 76 | 77 | // projectStore.addProject(newProject); 78 | // expect(projectStore.projects[0].id).toEqual(0); 79 | // expect(projectStore.projects[1].id).toEqual(1); 80 | 81 | // projectStore.deleteProject(projectStore.projects[1].id); 82 | // projectStore.addProject(newProject); 83 | // expect(projectStore.projects[0].id).toEqual(0); 84 | // expect(projectStore.projects[1].id).toEqual(1); 85 | 86 | // projectStore.deleteProject(projectStore.projects[0].id); 87 | // projectStore.addProject(newProject); 88 | // expect(projectStore.projects[0].id).toEqual(1); 89 | // expect(projectStore.projects[1].id).toEqual(0); 90 | // }); 91 | 92 | it("should be able to change project name", function() { 93 | let projects = callProjectStoreAction( 94 | { type: types.CHANGE_NAME, id: 0, newName: "debugProject2" } 95 | ); 96 | 97 | expect(projects[0].name).toEqual("debugProject2"); 98 | }); 99 | 100 | it("should throw error on changing project name to empty string", function() { 101 | expect(() => callProjectStoreAction({ type: types.CHANGE_NAME, id: 0, newName: "" })).toThrowError(); 102 | }); 103 | 104 | it("should be able to toggle project status", function() { 105 | let projects = callProjectStoreAction( 106 | { type: types.TOGGLE_ACTIVE, id: 0, newName: "debugProject2" } 107 | ); 108 | 109 | expect(projects[0].isActive).toBeFalsy(); 110 | }); 111 | }); -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/src/store/project-store.tsx: -------------------------------------------------------------------------------- 1 | // TODO: Refactor project-store 2 | 3 | import { IObservableArray } from "mobx"; 4 | import { types } from "mobx-state-tree"; 5 | import { ADD_PROJECT, DELETE_PROJECT, CHANGE_NAME, TOGGLE_ACTIVE } from "./project-store-action-type"; 6 | 7 | export const Project = types.model("Project", { 8 | id: types.number, 9 | name: types.string, 10 | isActive: types.optional(types.boolean, false) 11 | }); 12 | 13 | export const ProjectStore = types.model("ProjectStore", { 14 | projects: types.array(Project) 15 | }).actions((self) => ({ 16 | [ADD_PROJECT]({newProject}): void { 17 | if (!newProject.name || newProject.name.length == 0) { 18 | throw new Error("ProjectStore Model Action Error: new project name should not be empty"); 19 | } 20 | 21 | let id = getUniqueProjectId(self.projects); 22 | self.projects.push({ 23 | id: id, 24 | name: newProject.name, 25 | isActive: newProject.isActive 26 | } as IProject); 27 | }, 28 | 29 | [DELETE_PROJECT]({id}): void { 30 | let index = self.projects.findIndex(project => project.id == id); 31 | if (index == -1) { 32 | throw new Error("ProjectStore Model Action Error: project not found"); 33 | } 34 | 35 | self.projects.splice(index, 1); 36 | }, 37 | 38 | [CHANGE_NAME]({id, newName}): void { 39 | if (self.projects.findIndex(project => project.id == id) == -1) { 40 | throw new Error("ProjectStore Model Action Error: project not found"); 41 | } 42 | 43 | if (!newName || newName.length == 0) { 44 | throw new Error("Project Model Action Error: new name should not be empty"); 45 | } 46 | 47 | self.projects[id].name = newName; 48 | }, 49 | 50 | [TOGGLE_ACTIVE]({id}): void { 51 | if (self.projects.findIndex(project => project.id == id) == -1) { 52 | throw new Error("ProjectStore Model Action Error: project not found"); 53 | } 54 | 55 | self.projects[id].isActive = !self.projects[id].isActive; 56 | } 57 | })); 58 | 59 | function getUniqueProjectId (projects: IObservableArray): number { 60 | let id = 0; 61 | projects.map(project => project.id).forEach((currentId, currentIndex) => { 62 | if (currentId != currentIndex) { 63 | id = currentIndex; 64 | } 65 | }); 66 | 67 | if (projects.find(project => project.id == id)) { 68 | id = projects.length; 69 | } 70 | 71 | return id; 72 | }; 73 | 74 | export type IProject = typeof Project.Type; 75 | 76 | export type IProjectStore = typeof ProjectStore.Type; 77 | 78 | const projectStore = ProjectStore.create({ 79 | projects: [{ 80 | id: 0, 81 | name: "debugProject1", 82 | isActive: true 83 | } as typeof Project.Type] 84 | }); 85 | 86 | export default projectStore; -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "experimentalDecorators": true, 5 | "noEmitOnError": true, 6 | "removeComments": true, 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "target": "es5", 10 | "types": [ 11 | "@types/react", 12 | "@types/react-dom", 13 | "@types/react-test-renderer", 14 | "@types/jasmine" 15 | ], 16 | "lib": [ 17 | "es2015", 18 | "dom" 19 | ] 20 | }, 21 | "include": [ 22 | "src" 23 | ], 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /examples/redux-devtools-implementation/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/main.tsx", 3 | 4 | output: { 5 | filename: "bundle.js", 6 | path: __dirname + "/src", 7 | }, 8 | 9 | resolve: { 10 | extensions: [".tsx", ".js"] 11 | }, 12 | 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.tsx?$/, 17 | exclude: /node_modules/, 18 | loaders: ["ts-loader"] 19 | } 20 | ] 21 | } 22 | }; -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require("./webpack.config.js"); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: "", 6 | frameworks: [ 7 | "jasmine" 8 | ], 9 | plugins: [ 10 | require("karma-jasmine"), 11 | require("karma-chrome-launcher"), 12 | require("karma-webpack") 13 | ], 14 | 15 | files: [ 16 | "spec-finder.js" 17 | ], 18 | 19 | preprocessors: { 20 | "spec-finder.js": ["webpack"] 21 | }, 22 | 23 | webpack: { 24 | module: webpackConfig.module, 25 | resolve: webpackConfig.resolve 26 | }, 27 | 28 | webpackMiddleware: { 29 | noInfo: true 30 | }, 31 | 32 | port: 9876, 33 | colors: true, 34 | logLevel: config.LOG_INFO, 35 | autoWatch: true, 36 | browsers: [ 37 | "ChromeHeadless" 38 | ], 39 | singleRun: true 40 | }) 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mobx-mobxstatetree", 3 | "version": "1.0.0", 4 | "description": "A simple application created in React with Mobx and Mobx-State-Tree for learning purpose.", 5 | "scripts": { 6 | "build": "node ./node_modules/webpack/bin/webpack.js", 7 | "serve": "lite-server -c=bs-config.json", 8 | "prestart": "npm run build", 9 | "start": "npm run build | npm run serve", 10 | "test": "karma start karma.conf.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/samuelalvin/react-mobx-mobxstatetree" 15 | }, 16 | "keywords": [ 17 | "reactjs", 18 | "mobx", 19 | "mobx-react", 20 | "mobx-state-tree", 21 | "typescript" 22 | ], 23 | "author": "Samuel Hutama", 24 | "license": "MIT", 25 | "dependencies": { 26 | "@types/react": "^16.0.35", 27 | "@types/react-dom": "^16.0.3", 28 | "@types/react-test-renderer": "^16.0.0", 29 | "mobx": "^3.2.2", 30 | "mobx-react": "^4.2.2", 31 | "mobx-state-tree": "^0.10.2", 32 | "react": "^16.2.0", 33 | "react-dom": "^16.2.0", 34 | "react-test-renderer": "^16.2.0" 35 | }, 36 | "devDependencies": { 37 | "@types/jasmine": "^2.5.54", 38 | "jasmine-core": "^2.7.0", 39 | "karma": "^1.7.0", 40 | "karma-chrome-launcher": "^2.2.0", 41 | "karma-jasmine": "^1.1.0", 42 | "karma-webpack": "^2.0.4", 43 | "lite-server": "^2.3.0", 44 | "ts-loader": "^2.3.3", 45 | "typescript": "^2.4.2", 46 | "typescript-compiler": "^1.4.1-2", 47 | "webpack": "^3.5.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spec-finder.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./src', true, /.+\.spec\.tsx?$/); 2 | context.keys().forEach(context); -------------------------------------------------------------------------------- /src/app/app.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 3 | import App from "./app"; 4 | 5 | describe("app", function () { 6 | it("should be rendered without any problem", function () { 7 | let renderer = ReactShallowRenderer.createRenderer(); 8 | renderer.render(); 9 | 10 | let result = renderer.getRenderOutput(); 11 | expect(result).toBeDefined(); 12 | expect(result.type).toMatch("div"); 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ProjectList from "../project/project-list"; 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
    8 |

    Project List

    9 | 10 |
    11 | ); 12 | } 13 | } 14 | 15 | export default App; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /src/lib/react-test-utils-extended.tsx: -------------------------------------------------------------------------------- 1 | import * as TestUtils from "react-dom/test-utils"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | type ITestUtils = typeof TestUtils; 5 | 6 | interface ITestUtilsExtended extends ITestUtils { 7 | /** 8 | * Finds a DOM element of components in the rendered tree that are 9 | * DOM components with the DOM element name matching `elementName`. Returns undefined if no element matches `elementName`. 10 | */ 11 | findRenderedDOMComponentsWithName?(tree: React.Component, elementName: string): React.ReactInstance; 12 | 13 | /** 14 | * Finds all DOM elements of components in the rendered tree that are 15 | * DOM components with the DOM element name matching `elementName`. 16 | */ 17 | scryRenderedDOMComponentsWithName?(tree: React.Component, elementName: string): React.ReactInstance[]; 18 | } 19 | 20 | var TestUtilsExtended = TestUtils as ITestUtilsExtended; 21 | 22 | TestUtilsExtended.findRenderedDOMComponentsWithName = function (tree, elementName) { 23 | return TestUtils.findAllInRenderedTree(tree, function (inst) { 24 | return TestUtils.isDOMComponent(inst) && inst.getAttribute("name") == elementName; 25 | })[0]; 26 | } 27 | 28 | TestUtilsExtended.scryRenderedDOMComponentsWithName = function (tree, elementName) { 29 | return TestUtils.findAllInRenderedTree(tree, function (inst) { 30 | return TestUtils.isDOMComponent(inst) && inst.getAttribute("name") == elementName; 31 | }); 32 | } 33 | 34 | export default TestUtilsExtended; -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import App from "./app/app" 4 | import { Provider } from "mobx-react"; 5 | 6 | import projectStore, { Project } from "./store/project-store"; 7 | 8 | const mainApp = ( 9 | 10 | 11 | 12 | ); 13 | 14 | ReactDOM.render( 15 | mainApp, 16 | document.getElementById("app") 17 | ); -------------------------------------------------------------------------------- /src/project/project-details.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TestUtils from "react-dom/test-utils"; 3 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 4 | import { Project } from "../store/project-store"; 5 | import ProjectDetails, { IProjectListProps } from "./project-details"; 6 | 7 | const project = Project.create({ 8 | id: 0, 9 | name: "debugProject1" 10 | }); 11 | 12 | describe("project-details", function () { 13 | let deleteProject: jasmine.Spy; 14 | 15 | beforeEach(function() { 16 | deleteProject = jasmine.createSpy("deleteProject"); 17 | }); 18 | 19 | it("should be created without any problem", function () { 20 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 21 | expect(projectDetails).toBeDefined(); 22 | }); 23 | 24 | it("should be rendered without any problem", function () { 25 | let renderer = ReactShallowRenderer.createRenderer(); 26 | renderer.render(); 27 | 28 | let result = renderer.getRenderOutput(); 29 | expect(result).toBeDefined(); 30 | expect(result.type).toMatch("li"); 31 | expect((result.props.children[0] as React.ReactElement).type).toMatch("span"); 32 | }); 33 | 34 | it("should show span on nonEditMode", function () { 35 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 36 | let span = TestUtils.findRenderedDOMComponentWithTag(projectDetails, "span"); 37 | let inputs = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "input"); 38 | 39 | expect(span).toBeDefined(); 40 | expect(inputs.length).toEqual(0); 41 | }); 42 | 43 | it("should show input on editMode", function () { 44 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 45 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "button"); 46 | let editButton = buttons[0]; 47 | 48 | TestUtils.Simulate.click(buttons[0]); 49 | 50 | let inputs = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "input"); 51 | expect(inputs.length).toBeGreaterThan(0); 52 | }); 53 | 54 | it("should be able to delete a project", function () { 55 | let projectDetails = TestUtils.renderIntoDocument() as React.Component; 56 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(projectDetails, "button"); 57 | let deleteButton = buttons[1]; 58 | 59 | TestUtils.Simulate.click(deleteButton); 60 | expect(deleteProject).toHaveBeenCalledTimes(1); 61 | }); 62 | }); -------------------------------------------------------------------------------- /src/project/project-details.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { observable } from "mobx"; 4 | 5 | import { IProject } from "../store/project-store"; 6 | 7 | export interface IProjectListProps { 8 | project: IProject 9 | onDeletion(id: number): void; 10 | } 11 | 12 | @observer 13 | class ProjectDetails extends React.Component { 14 | @observable private editMode: boolean = false; 15 | 16 | constructor(props){ 17 | super(props); 18 | this.toggleEditMode = this.toggleEditMode.bind(this); 19 | this.getProjectForm = this.getProjectForm.bind(this); 20 | } 21 | 22 | toggleEditMode(): void { 23 | this.editMode = !this.editMode; 24 | } 25 | 26 | getProjectForm(project: IProject): JSX.Element { 27 | if (this.editMode) { 28 | return ( 29 |
    30 | 31 | 32 |
    33 | ); 34 | } 35 | 36 | return ( 37 | {project.name} - {project.isActive ? "active" : "inactive"} 38 | ); 39 | } 40 | 41 | render(): JSX.Element { 42 | const { project } = this.props; 43 | return ( 44 |
  • 45 | {this.getProjectForm(project)} 46 | 47 | 48 |
  • 49 | ); 50 | } 51 | } 52 | 53 | export default ProjectDetails; -------------------------------------------------------------------------------- /src/project/project-list.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactShallowRenderer from "react-test-renderer/shallow"; 3 | import ProjectList, { IProjectListProps } from "./project-list"; 4 | import { ProjectStore, IProjectStore, IProject } from "../store/project-store"; 5 | import TestUtilsExtended from "../lib/react-test-utils-extended"; 6 | 7 | function initializeProjectStore(): IProjectStore { 8 | return ProjectStore.create({ 9 | projects: [{ 10 | id: 0, 11 | name: "debugProject1", 12 | isActive: true 13 | } as IProject] 14 | }); 15 | }; 16 | 17 | describe("project-list", function () { 18 | it("should be created without any problem", function () { 19 | let projectStore = initializeProjectStore(); 20 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 21 | expect(projectList).toBeDefined(); 22 | }); 23 | 24 | it("should display projects", function(){ 25 | let projectStore = initializeProjectStore(); 26 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 27 | let projects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 28 | expect(projects.length).toEqual(projectStore.projects.length); 29 | }); 30 | 31 | it("should be able to add a project", function() { 32 | let projectStore = initializeProjectStore(); 33 | let projectList = TestUtilsExtended.renderIntoDocument() as React.Component; 34 | let nameInput = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "newProjectNameInput"); 35 | let checkBoxInput = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "newProjectStatusInput"); 36 | let addButton = TestUtilsExtended.findRenderedDOMComponentsWithName(projectList, "addProjectButton"); 37 | let projects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 38 | expect(projects.length).toEqual(1); 39 | 40 | TestUtilsExtended.Simulate.change(nameInput, { 41 | target: { 42 | value : "debugProject2" 43 | } 44 | } as any); 45 | TestUtilsExtended.Simulate.click(addButton); 46 | 47 | projects = TestUtilsExtended.scryRenderedDOMComponentsWithTag(projectList, "li"); 48 | expect(projects.length).toEqual(2); 49 | }); 50 | }); -------------------------------------------------------------------------------- /src/project/project-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, inject } from "mobx-react"; 3 | 4 | import { IProjectStore, Project, IProject } from "../store/project-store"; 5 | import ProjectDetails from "./project-details"; 6 | 7 | export interface IProjectListProps { 8 | projectStore?: IProjectStore 9 | } 10 | 11 | @inject("projectStore") 12 | @observer 13 | class ProjectList extends React.Component { 14 | private newProject: IProject; 15 | 16 | constructor(props) { 17 | super(props); 18 | 19 | this.newProject = Project.create({ 20 | id: -1, 21 | name: "" 22 | }); 23 | 24 | this.deleteProject = this.deleteProject.bind(this); 25 | this.addProject = this.addProject.bind(this); 26 | } 27 | 28 | addProject(newProject: IProject): void { 29 | const { projectStore } = this.props; 30 | projectStore.addProject(newProject); 31 | } 32 | 33 | deleteProject(id: number): void { 34 | const { projectStore } = this.props; 35 | projectStore.deleteProject(id); 36 | } 37 | 38 | render() { 39 | const { projectStore } = this.props; 40 | return ( 41 |
    42 |
      43 | {projectStore.projects.map((project) => ())} 44 |
    45 |
    46 | 47 | 48 |
    49 | 50 |
    51 | ); 52 | } 53 | } 54 | 55 | export default ProjectList; -------------------------------------------------------------------------------- /src/store/project-store.spec.tsx: -------------------------------------------------------------------------------- 1 | import ProjectList from "../project/project-list"; 2 | import { ProjectStore, IProjectStore, IProject } from "../store/project-store"; 3 | 4 | function initializeProjectStore(): IProjectStore { 5 | return ProjectStore.create({ 6 | projects: [{ 7 | id: 0, 8 | name: "debugProject1", 9 | isActive: true 10 | } as IProject] 11 | }); 12 | }; 13 | 14 | describe("project-store", function () { 15 | it("should be initialized without any problem", function () { 16 | let projectStore = initializeProjectStore(); 17 | expect(projectStore).toBeDefined(); 18 | expect(projectStore.projects.length).toEqual(1); 19 | expect(projectStore.projects[0].name).toMatch("debugProject1"); 20 | }); 21 | 22 | it("should be able to add project", function () { 23 | let projectStore = initializeProjectStore(); 24 | let newProject = { 25 | name: "debugProject2", 26 | isActive: true 27 | } as IProject; 28 | 29 | projectStore.addProject(newProject); 30 | expect(projectStore.projects.length).toEqual(2); 31 | expect(projectStore.projects[1].name).toMatch("debugProject2"); 32 | expect(projectStore.projects[1].isActive).toBeTruthy(); 33 | }); 34 | 35 | it("should throw error on adding project with empty name", function () { 36 | let projectStore = initializeProjectStore(); 37 | let newProject = { 38 | name: "", 39 | isActive: true 40 | } as IProject; 41 | 42 | expect(() => projectStore.addProject(newProject)).toThrowError(); 43 | }); 44 | 45 | it("should be able to delete project", function () { 46 | let projectStore = initializeProjectStore(); 47 | projectStore.deleteProject(projectStore.projects[0].id); 48 | expect(projectStore.projects.length).toEqual(0); 49 | }); 50 | 51 | it("should throw error on deleting nonexistant project", function () { 52 | let projectStore = initializeProjectStore(); 53 | expect(() => projectStore.deleteProject(99)).toThrowError(); 54 | }); 55 | 56 | it("should be able to get unique id", function() { 57 | let projectStore = initializeProjectStore(); 58 | let newProject = { 59 | name: "debugProject2", 60 | isActive: true 61 | } as IProject; 62 | 63 | projectStore.addProject(newProject); 64 | expect(projectStore.projects[0].id).toEqual(0); 65 | expect(projectStore.projects[1].id).toEqual(1); 66 | 67 | projectStore.deleteProject(projectStore.projects[1].id); 68 | projectStore.addProject(newProject); 69 | expect(projectStore.projects[0].id).toEqual(0); 70 | expect(projectStore.projects[1].id).toEqual(1); 71 | 72 | projectStore.deleteProject(projectStore.projects[0].id); 73 | projectStore.addProject(newProject); 74 | expect(projectStore.projects[0].id).toEqual(1); 75 | expect(projectStore.projects[1].id).toEqual(0); 76 | }); 77 | 78 | it("should be able to change project name", function() { 79 | let projectStore = initializeProjectStore(); 80 | projectStore.projects[0].changeName("debugProject2"); 81 | expect(projectStore.projects[0].name).toEqual("debugProject2"); 82 | }); 83 | 84 | it("should throw error on changing project name to empty string", function() { 85 | let projectStore = initializeProjectStore(); 86 | expect(() => projectStore.projects[0].changeName("")).toThrowError(); 87 | }); 88 | 89 | it("should be able to toggle project status", function() { 90 | let projectStore = initializeProjectStore(); 91 | projectStore.projects[0].toggleActive(); 92 | expect(projectStore.projects[0].isActive).toBeFalsy(); 93 | 94 | projectStore.projects[0].toggleActive(); 95 | expect(projectStore.projects[0].isActive).toBeTruthy(); 96 | }); 97 | }); -------------------------------------------------------------------------------- /src/store/project-store.tsx: -------------------------------------------------------------------------------- 1 | import { IObservableArray } from "mobx"; 2 | import { types } from "mobx-state-tree"; 3 | 4 | export const Project = types.model("Project", { 5 | id: types.number, 6 | name: types.string, 7 | isActive: types.optional(types.boolean, false) 8 | }).actions((self) => ({ 9 | changeName(newName: string): void { 10 | if (!newName || newName.length == 0) { 11 | throw new Error("Project Model Action Error: new name should not be empty"); 12 | } 13 | 14 | self.name = newName; 15 | }, 16 | 17 | toggleActive(): void { 18 | self.isActive = !self.isActive; 19 | } 20 | })); 21 | 22 | export const ProjectStore = types.model("ProjectStore", { 23 | projects: types.array(Project) 24 | }).actions((self) => ({ 25 | addProject(newProject: IProject): void { 26 | if (!newProject.name || newProject.name.length == 0) { 27 | throw new Error("ProjectStore Model Action Error: new project name should not be empty"); 28 | } 29 | 30 | let id = getUniqueProjectId(self.projects); 31 | self.projects.push({ 32 | id: id, 33 | name: newProject.name, 34 | isActive: newProject.isActive 35 | } as IProject); 36 | }, 37 | 38 | deleteProject(id: number): void { 39 | let index = self.projects.findIndex(project => project.id == id); 40 | if (index == -1) { 41 | throw new Error("ProjectStore Model Action Error: project not found"); 42 | } 43 | 44 | self.projects.splice(index, 1); 45 | } 46 | })); 47 | 48 | function getUniqueProjectId (projects: IObservableArray): number { 49 | let id = 0; 50 | projects.map(project => project.id).forEach((currentId, currentIndex) => { 51 | if (currentId != currentIndex) { 52 | id = currentIndex; 53 | } 54 | }); 55 | 56 | if (projects.find(project => project.id == id)) { 57 | id = projects.length; 58 | } 59 | 60 | return id; 61 | }; 62 | 63 | export type IProject = typeof Project.Type; 64 | 65 | export type IProjectStore = typeof ProjectStore.Type; 66 | 67 | const projectStore = ProjectStore.create({ 68 | projects: [{ 69 | id: 0, 70 | name: "debugProject1", 71 | isActive: true 72 | } as typeof Project.Type] 73 | }); 74 | 75 | export default projectStore; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "experimentalDecorators": true, 5 | "noEmitOnError": true, 6 | "removeComments": true, 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "target": "es5", 10 | "types": [ 11 | "@types/react", 12 | "@types/react-dom", 13 | "@types/react-test-renderer", 14 | "@types/jasmine" 15 | ], 16 | "lib": [ 17 | "es2015", 18 | "dom" 19 | ] 20 | }, 21 | "include": [ 22 | "src" 23 | ], 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/main.tsx", 3 | 4 | output: { 5 | filename: "bundle.js", 6 | path: __dirname + "/src", 7 | }, 8 | 9 | resolve: { 10 | extensions: [".tsx", ".js"] 11 | }, 12 | 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.tsx?$/, 17 | exclude: /node_modules/, 18 | loaders: ["ts-loader"] 19 | } 20 | ] 21 | } 22 | }; --------------------------------------------------------------------------------