MVC pattern with decorator

Edit

In koa-cola we can write mvc by using es7's decorator. Controller have to be defined with the provided decorator (as it relates to the router related definition), and the model and view layers are not forced to be defined by the decorator as the demo following.

Controller

Use decorator to inject dependencies. In the router layer, the decorators include router, middleware, response and view.
In the response phase, the decorators including koa.Context, param, response, request, etc. For example. The following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const {
Controller, Get, Use, Param, Body, Delete, Put, Post, QueryParam, View, Ctx, Response
} = require('koa-cola/client');
import Ok from '../responses/ok';
@Controller('')
class FooController {
@Get('/some_api') // define router
@Response(Ok) // define return data format of API
some_api (@Ctx() ctx, @QueryParam() param : any) {
// inject ctx & param
// The data return the format defined by "Ok"
return {
foo : 'bar'
}
}
@Get('/some_page') // define router
@View('some_page') // this router use the page "some_page.tsx" inside "/views/pages/" to render the view
some_page (@Ctx() ctx, @QueryParam() param : any) { // inject ctx & param
// Initialize the data.
// Data will be injected into react component's props,
return {
foo : 'bar'
}
}
}

Ok Response

1
2
3
4
5
6
7
8
9
10
import * as Koa from 'koa';
export default function Ok(ctx : Koa.Context, data){
ctx.status = 200;
if(data){
ctx.body = {
code : 200,
result : data
};
}
}

View

Page's view components can use different types of react components

React.Component

1
2
3
4
5
6
7
8
9
10
11
12
class Index extends React.Component<Props, States> {
constructor(props: Props) {
super(props);
}
static defaultProps = {
};
render() {
return <h1>Wow koa-cola!</h1>
}
};
export default Index

stateless component

1
2
3
export default function({some_props}) {
return <h1>Wow koa-cola!</h1>
}

react-redux component

1
2
3
4
5
6
7
8
import { connect } from 'react-redux'
const Index = function({some_props}) {
return <h1>Wow koa-cola!</h1>
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Index)

Cola decorator component

Use Cola decorators to create react-redux base components.

If there's children components also created with Cola decorator, you need to use include decorator to include them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import AddTodo from '../official-demo/containers/AddTodo';
import FilterLink from '../official-demo/containers/FilterLink';
import VisibleTodoList from '../official-demo/containers/VisibleTodoList';
const {
Cola
include
} = require('koa-cola/client');
@Cola({
initData : {
todosData : async ({ params, helpers, store: { dispatch } }) => {
const api = new GetTodoList({});
const data = await api.fetch(helpers.ctx);
dispatch({
type: 'INIT_TODO',
data: data.result.result
});
return data.result.result;
}
},
reducer : {
todos,
visibilityFilter
}
})
@include({ AddTodo, FilterLink, VisibleTodoList })
class ColastyleDemo extends React.Component<Props, States> {
constructor(props: Props) {
super(props);
}
render() {
return <App />;
}
}
export default ColastyleDemo;

Custom header & bundle packing

When koa-cola render view in server side, it will be looking for views/pages/layout.ts as the page layout.
if layout.ts file does not exist, the view component will render directly.
If the view component uses the doNotUseLayout decorator, the page will not use layout.ts, and you probably need header and bundle decorators to define header and resource.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as React from 'react';
const {
header, bundle, doNotUseLayout
} = require('koa-cola/client');
@doNotUseLayout
@bundle([
"/bundle.js",
"/test.js"
])
@header(() => {
return <head>
<meta name="viewport" content="width=device-width" />
</head>
})
function Page (){
return <h1>koa-cola</h1>
};
export default Page

Model

create "user.ts" directly under the directory "api/models"

1
2
3
4
5
import * as mongoose from 'mongoose'
export default mongoose.model('user', new mongoose.Schema({
name : String,
email : String
}))

Then you can use it in other code:

1
const user = await app.models.user.find({name : 'harry'})

koa-cola style to write the model

First create schema user.ts in the api/schemas directory:

1
2
3
4
5
6
7
8
9
10
export const userSchema = function(mongoose){
return {
name: {
type : String
},
email : {
type : String
}
}
}

Create a model user.ts in api/models:

1
2
3
import * as mongoose from 'mongoose'
import userSchema from '../schemas/user'
export default mongoose.model('user', new mongoose.Schema(userSchema(mongoose)))

Use decorator to define model also works well, we can define the relevant hook if needeed. More details can visit mongoose-decorators

1
2
3
4
5
import { todoListSchema } from '../schemas/todoList';
const { model } = app.decorators.model;
@model(todoListSchema(app.mongoose))
export default class TodoList {}

Generate model's schema using cli

koa-cola schema will automatically generate model interface in typings/schema.ts.

Then you can enjoy the convenience of vscode's intellisense by defining the types of typescript in your code.

1
2
import {userSchema} from './typings/schema'
const user : userSchema = await app.models.user.find({name : 'harry'})

As mentioned earlier, the reason we need to define the model schema in api/schemas, in addition to generate schema interface, you can use the schema in both browser and server side. more detail you can visit document

koa-cola provides universal api interface definitions for both front and back end, such as GetTodoList api definition in the todolist demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { todoListSchema } from './typings/schema';
import { ApiBase, apiFetch } from 'koa-cola';
export class GetTodoList extends ApiBase<
{
// Parameter Type
},
{
code: number;
result: [todoListSchema];
},
{
// Abnormal definition
}
> {
constructor(body) {
super(body);
}
url: string = '/api/getTodoList';
method: string = 'get';
}

Use api in the code, and get the convenience provided by ts:

1
2
const api = new GetTodoList({});
const data = await api.fetch(helpers.ctx);

Drawing
Drawing

1
2
3
4
5
6
7
8
9
10
11
12
13
import { testSchema } from './typings/schema';
import { ApiBase, apiFetch } from 'koa-cola'
export interface ComposeBody{
foo : string,
bar? : number
}
export class Compose extends ApiBase<ComposeBody, testSchema, {}>{
constructor(body : ComposeBody){
super(body)
}
url : string = '/compose'
method : string = 'post'
}

Drawing