-
Notifications
You must be signed in to change notification settings - Fork 42
redstone_mvc
Checkout the code shown here at example folder.
MVC Plugin for Redstone allows you to create routes that render mustache
templates using the return object as a model. With the custom @ViewController
annotation you can both specify both the http route the function will
respond to and the template that will be rendered.
To setup the plugin, you need add mvc.mvcPlugin
to app.addPlugin
on
the main
function.
import 'package:redstone/server.dart' as app;
import 'package:redstone_mapper/mapper.dart';
import 'dart:math' as math;
import 'package:redstone_mvc/redstone_mvc.dart' as mvc;
main() {
app.addPlugin(mvc.mvcPluggin);
app.setupConsoleLog();
app.start(port: 9090);
}
MVC works by encoding the object model returned by the function with redstone_mapper
into a Map
which is passed to mustache
and then returned as shelf.Response
with it Content-Type
header set to text/html
. You need to make sure that your model has the @Field()
from redstone_mapper
so they can properly be encoded and rendered.
class Example {
@Field() String title;
@Field() String description;
}
##ViewController
ViewController
extends Route
so you can easy replace the routes you want to render html with a ViewController
. The next examples shows how to use the template
parameter to render a simple mustache template at route /stringTemplate
using the information from the Example
instance model returned by the route function stringTemplate
.
@mvc.ViewController('/stringTemplate', template:
'''
<div>
<h2>{{title}}</h2>
<p>{{description}}</p>
</div>
''')
stringTemplate() => new Example()
..title = "MVC"
..description = "MVC is very easy with Redstone";
Normally however, you want to render an html file template. For this you can use the filePath
parameter to specify the path of your file. By default it will expect that the template is an .html
file, but if you need a diferent extension you can use the extension
parameter to specify it.
In the nex example we are rendering the /example/lib/template.html
on route /fileTemplate
using an Example
instance as a model.
@mvc.ViewController('/fileTemplate', filePath: '/example/lib/template')
fileTemplate() => new Example()
..title = "MVC"
..description = "MVC is very easy with Redstone";
If the route's urlTemplate
path doesn't take you all the way to your template file, you use subpath
to complete the remaining parts. Setting subpath
will have no effect if you specified filePath
.
@mvc.ViewController ('/example/lib', subpath: '/template')
subPath() => new Example()
..title = "Using subpath"
..description = "Just appends to the path";
If filePath
and your route path match, you should unspecify [filePath] and ViewController
will use the route's path to find your template file. Frameworks such as ASP.net MVC enforce folder structure, while Redstone MVC doesnt its is a good practice to order your template in a folder heirachy similar to the route heirarchy.
@mvc.ViewController('/example/lib/template')
viewController() => new Example()
..title = "MVC"
..description = "MVC is very easy with Redstone";
You can also set a folder directory as root
so you don't polute your route. root
will prepend to both filePath
and your route's urlTemplate
path depending on which is used.
@mvc.ViewController('/template', root: '/example/lib')
viewControllerRoot() => new Example()
..title = "MVC"
..description = "MVC is very easy with Redstone";
You can use a ViewController
inside a app.Group
since it only prepends a section to your route's path. However you might also want to specify a common root
directory to all your filaPath
s or routes, for this you can use the GroupController
annotation. GroupController
extends Group
and takes the normal urlPrefix
as first argument and an optional root
parameters.
@mvc.GroupController('/info', root: '/example/lib')
class ExampleService1
{
@mvc.ViewController ('/A')
viewA () => new Example()
..title = "Route A"
..description = "Some description of A";
@mvc.ViewController ('/B')
viewB () => new Example()
..title = "Route B"
..description = "Some description of B";
}
If you want to reuse or have more control over which html get rendered, you can set a root
in the GroupController
and filePath
in each ViewController
.
In the next example, routes will be /info2/A
and /info2/B
, but for both their template will be /example/lib/template.html
.
@mvc.GroupController('/info2', root: '/example/lib')
class ExampleService2
{
@mvc.ViewController ('/A', filePath: '/template')
viewA () => new Example()
..title = "Route A"
..description = "Some description of A";
@mvc.ViewController ('/B', filePath: '/template')
viewB () => new Example()
..title = "Route B"
..description = "Some description of B";
}
Note that you can also set includeRoot
to false
if you want to ignore the root
directory. If you want to bind to the groups route, you can use @mvc.ViewControllerDefault
that extends from @app.DefaultRoute
for that purpose.
You can set the filePath
dynamically at runtime by returning a Model_Path
. Model_Path
takes an Object model
, String path
and optionally a String extension
that defualts to 'html'
.
@mvc.ViewController ('/randomTemplate', root: '/example/lib/info')
dynamicFilePath ()
{
var filePath = new math.Random().nextBool() ? '/A' : '/B';
var model = new Example()
..title = "Dynamic File Path"
..description = 'If you return a Model_Path you can set the templates path dynamically';
return new mvc.Model_Path(model, filePath);
}
Model_StringTemplate
and Model_Template
exist to provide similar features, but you can also provide you own class that implements the mvc.Renderable
interface.
Thanks to the use of annotations the logic of the controller is decoupled from viewing mechanism to the extent one can create different rendering formats and reuse the same function. To maintain a similar experience as with the ViewController
but targeting JSON as an output you can use @DataController
. DataController
is almost equivalent simultaneously using @app.Route(...)
and @Encode()
on the same function/method, except that it also works with objects that implement the mvc.Renderable
interface so you don't have to change your code if you are switching from a ViewController
to a DataController
.
In the next example, we create a ViewController
using the method viewUpper
that decodes an Example
from a form
, transforms all its fields to upper case and renders the a view. Then we create a DataController
with the exact same functionality but using JSON, were we get the implementation for free since we just reuse the previous method.
@mvc.GroupController('/mixing', root: '/lib')
class ExampleService2 {
@mvc.ViewController ('/view', filePath: '/template', methods: const[app.POST], allowMultipartRequest: true)
viewUpper (@Decode(from: const[app.FORM]) Example example) => example
..title = example.title.toUpperCase()
..description = example.description.toUpperCase();
@mvc.DataController ('/json', methods: const[app.POST])
jsonUpper (@Decode() Example example) => viewUpper(example);
}
While it might be better one the long-run to have separate classes and APIs to distinguish MVC from REST, mixing both might be a nice solution for certain problems, especially when you are still exploring the problem. As a use case, imagine a mostly REST service where you sometimes what to respond html
to visualize certain data. For this you could maybe create a ViewController
route on /resource/:id/render
, the implementation of the function could just reuse the GET
method on /resource/:id
similar to the previous example.
-
Quick Start Guide
-
Reference
-
Serialization
-
Databases
-
MVC
-
Web Sockets