├── .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 |
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 |
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 |
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 | };
--------------------------------------------------------------------------------