Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

initial commit

  • Loading branch information...
commit 22c0fba6136c53e9afed7d3c512495224d7b4287 0 parents
Victor Savkin authored
5 .gitignore
... ... @@ -0,0 +1,5 @@
  1 +pubspec.lock
  2 +packages
  3 +test/packages
  4 +.idea
  5 +.project
23 LICENCE
... ... @@ -0,0 +1,23 @@
  1 +Copyright (c) 2012, Victor Savkin
  2 +All rights reserved.
  3 +
  4 +Redistribution and use in source and binary forms, with or without
  5 +modification, are permitted provided that the following conditions are met:
  6 + * Redistributions of source code must retain the above copyright
  7 + notice, this list of conditions and the following disclaimer.
  8 + * Redistributions in binary form must reproduce the above copyright
  9 + notice, this list of conditions and the following disclaimer in the
  10 + documentation and/or other materials provided with the distribution.
  11 + * The name of the copyright owner may be used to endorse or promote products
  12 + derived from this software without specific prior written permission.
  13 +
  14 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  15 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  16 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  17 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY
  18 +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  19 +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  20 +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  21 +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  22 +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  23 +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 README.md
Source Rendered
... ... @@ -0,0 +1,27 @@
  1 +# SimpleMVP
  2 +
  3 +SimpleMVP is a framework for writing single-page applications in Dart. Similar to Backbone it has the following key components:
  4 +
  5 +* Models
  6 +* ModelLists (Collections in Backbone)
  7 +* Presenters (Views in Backbone)
  8 +
  9 +## Example App
  10 +
  11 +There is a task list application using SimpleMVP in the example folder.
  12 +
  13 +To try it out:
  14 +
  15 +* Run: dart dummy_server.dart
  16 +* Open Dartium: localhost:8080/task_list.html
  17 +
  18 +The application illustrates most of the features of SimpleMVP, and it's about 100 lines long (including all templates). Check out `task_list.dart` to see how it's implemented.
  19 +
  20 +## Video
  21 +
  22 +I've put together a 15-minute video showing how to build a TODO app using SimpleMVP.
  23 +[Building a TODO app using SimpleMVP](https://vimeo.com/49728673)
  24 +
  25 +## Learning Dart
  26 +
  27 +Even if you don't end up using SimpleMVP, you can still use it as an example of an MV* framework written in Dart.
95 example/dummy_server.dart
... ... @@ -0,0 +1,95 @@
  1 +#import("dart:io");
  2 +#import("dart:json");
  3 +#import("dart:math");
  4 +
  5 +final HOST = "127.0.0.1";
  6 +final PORT = 8080;
  7 +
  8 +void main() {
  9 + var server = new HttpServer();
  10 + server.addRequestHandler((request) => true, (request, response){
  11 + new RequestHandler(request,response).process();
  12 + }
  13 + );
  14 + server.listen(HOST, PORT);
  15 +}
  16 +
  17 +class RequestHandler {
  18 + HttpRequest request;
  19 + HttpResponse response;
  20 +
  21 + RequestHandler(this.request, this.response);
  22 +
  23 + void process() {
  24 + var uri = request.uri;
  25 + print("${request.method} ${uri}");
  26 +
  27 + if (uri.startsWith("/api/tasks") && request.method == "GET"){
  28 + _render("text/json", _items());
  29 +
  30 + } else if (uri.startsWith("/api/tasks") && request.method == "POST"){
  31 + _renderNewRecord();
  32 +
  33 + } else if (uri.startsWith("/api/task")){
  34 + _renderUpdatedRecord();
  35 +
  36 + } else if (uri.endsWith(".dart")) {
  37 + _renderDartFile();
  38 +
  39 + } else if (uri.endsWith(".css")){
  40 + var file = new File("example/${_filename(uri)}");
  41 + _render("text/css", file.readAsTextSync());
  42 +
  43 + } else if (uri.endsWith(".html")){
  44 + var file = new File("example/${_filename(uri)}");
  45 + _render("text/html", file.readAsTextSync());
  46 + }
  47 + }
  48 +
  49 + _renderNewRecord(){
  50 + var s = new StringInputStream(request.inputStream);
  51 + s.onData = (){
  52 + var data = s.read();
  53 + print("body: ${data}");
  54 +
  55 + var map = JSON.parse(data);
  56 + map["id"] = new Random().nextInt(10000);
  57 + _render("text/json", JSON.stringify(map));
  58 + };
  59 + }
  60 +
  61 + _renderUpdatedRecord(){
  62 + var s = new StringInputStream(request.inputStream);
  63 + s.onData = (){
  64 + var data = s.read();
  65 + print("body: ${data}");
  66 + _render("text/json", data);
  67 + };
  68 + }
  69 +
  70 + _renderDartFile(){
  71 + var uri = request.uri;
  72 +
  73 + var libFile = new File(_filename(uri));
  74 + var exampleFile = new File("example/${_filename(uri)}");
  75 +
  76 + if(libFile.existsSync()){
  77 + _render("application/dart", libFile.readAsTextSync());
  78 + } else {
  79 + _render("application/dart", exampleFile.readAsTextSync());
  80 + }
  81 + }
  82 +
  83 + _filename(uri) => uri.substring(uri.indexOf("/") + 1);
  84 +
  85 + _render(contentType, body){
  86 + response.headers.set(HttpHeaders.CONTENT_TYPE, "$contentType; charset=UTF-8");
  87 + response.outputStream.writeString(body);
  88 + response.outputStream.close();
  89 + }
  90 +
  91 + _items() {
  92 + var r = [{"id": 1, "text": "Task 1", "status" : "inProgress"}, {"id": 2, "text": "Task 2", "status" : "inProgress"}];
  93 + return JSON.stringify(r);
  94 + }
  95 +}
20 example/task_list.css
... ... @@ -0,0 +1,20 @@
  1 +li {
  2 + list-style-type: none;
  3 + margin-top: 15px;
  4 + /*background-color: #F8F8F8;*/
  5 + font-size: 15px;
  6 + padding-left: 5px;
  7 +}
  8 +
  9 +span.done-true {
  10 + text-decoration: line-through;
  11 +}
  12 +
  13 +.actions {
  14 + float: right;
  15 +}
  16 +
  17 +input.task-text {
  18 + width: 400px;
  19 + margin-bottom: 0px;
  20 +}
137 example/task_list.dart
... ... @@ -0,0 +1,137 @@
  1 +#import('dart:html');
  2 +#import('../lib/simple_mvp.dart', prefix: "smvp");
  3 +
  4 +class Tasks extends smvp.ModelList<Task>{
  5 + final rootUrl = "/api/tasks";
  6 +
  7 + makeInstance(attrs, list) => new Task(attrs, list);
  8 +}
  9 +
  10 +class Task extends smvp.Model {
  11 + Task(attrs, modelList): super(attrs, modelList);
  12 + Task.fromText(String text): this({"text": text, "status": "inProgress"}, null);
  13 +
  14 + final rootUrl = "/api/task";
  15 + final createUrl = "/api/tasks";
  16 +
  17 + get isCompleted => status == "completed";
  18 +
  19 + complete(){
  20 + status = "completed";
  21 + save();
  22 + }
  23 +
  24 + inProgress(){
  25 + status = "inProgress";
  26 + save();
  27 + }
  28 +}
  29 +
  30 +
  31 +
  32 +
  33 +oneTaskTemplate(c) => """
  34 +<div class="well well-small">
  35 + <span class="text">${c.text}</span>
  36 + <span class="actions">
  37 + <a class="complete" href="#">[DONE]</a>
  38 + <a class="delete" href="#">[DELETE]</a>
  39 + </span>
  40 +</div>
  41 +""";
  42 +
  43 +newTaskTemplate(c) => """
  44 +<div class="well well-small">
  45 + <input type="text" class="task-text"/>
  46 + <button class="btn">Create!</button>
  47 +</div>
  48 +""";
  49 +
  50 +taskListTemplate(c) => """
  51 +<div id="tasks">
  52 +</div>
  53 +""";
  54 +
  55 +
  56 +
  57 +
  58 +class TaskPresenter extends smvp.Presenter<Task> {
  59 + TaskPresenter(task, el) : super(task, el, oneTaskTemplate);
  60 +
  61 + subscribeToModelEvents() {
  62 + model.on.change.add(_onChange);
  63 + }
  64 +
  65 + _onChange(e){
  66 + render();
  67 + el.query(".text").classes.add("done-${model.isCompleted}");
  68 + }
  69 +
  70 + get events => {
  71 + "click a.delete": _onDelete,
  72 + "click a.complete": _onComplete
  73 + };
  74 +
  75 + _onDelete(event) => model.destroy();
  76 + _onComplete(event) => model.isCompleted ? model.inProgress() : model.complete();
  77 +}
  78 +
  79 +class NewTaskPresenter extends smvp.Presenter<Tasks> {
  80 + NewTaskPresenter(tasks, el) :super(tasks, el, newTaskTemplate);
  81 +
  82 + get events => {
  83 + "click button": _addNewTask,
  84 + "keypress input": _maybeAddNewTask
  85 + };
  86 +
  87 + _maybeAddNewTask(event){
  88 + if(event.keyIdentifier == "Enter"){
  89 + _createTask();
  90 + }
  91 + }
  92 +
  93 + _addNewTask(event){
  94 + _createTask();
  95 + }
  96 +
  97 + _createTask(){
  98 + var textField = el.query(".task-text");
  99 +
  100 + var task = new Task.fromText(textField.value);
  101 + model.add(task);
  102 + task.save();
  103 +
  104 + textField.value = "";
  105 + }
  106 +}
  107 +
  108 +class TasksPresenter extends smvp.Presenter<Tasks>{
  109 + TasksPresenter(tasks, el) : super(tasks, el, taskListTemplate){
  110 + model.fetch();
  111 + }
  112 +
  113 + subscribeToModelEvents(){
  114 + model.on.load.add(_rerenderTasks);
  115 + model.on.insert.add(_rerenderTasks);
  116 + model.on.remove.add(_rerenderTasks);
  117 + }
  118 +
  119 + _rerenderTasks(event){
  120 + var t = el.query("#tasks");
  121 + t.elements.clear();
  122 +
  123 + _buildPresenters().forEach((v){
  124 + t.elements.add(v.render().el);
  125 + });
  126 + }
  127 +
  128 + _buildPresenters() => model.map((t) => new TaskPresenter(t, new Element.tag("li")));
  129 +}
  130 +
  131 +main() {
  132 + var tasks = new Tasks();
  133 + var newTaskPresenter = new NewTaskPresenter(tasks, new Element.tag("div"));
  134 + var tasksPresenter = new TasksPresenter(tasks, new Element.tag("div"));
  135 +
  136 + query("#container").elements.addAll([newTaskPresenter.render().el, tasksPresenter.render().el]);
  137 +}
20 example/task_list.html
... ... @@ -0,0 +1,20 @@
  1 +<!DOCTYPE html>
  2 +
  3 +<html>
  4 +<head>
  5 + <meta charset="utf-8">
  6 + <title>Dart_clientside</title>
  7 + <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/css/bootstrap-combined.min.css">
  8 + <link rel="stylesheet" href="task_list.css">
  9 +</head>
  10 +<body>
  11 +<div class="hero-unit">
  12 + <div id="container">
  13 + <h1>Task List</h1>
  14 + </div>
  15 +</div>
  16 +
  17 +<script type="application/dart" src="task_list.dart"></script>
  18 +<script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script>
  19 +</body>
  20 +</html>
12 lib/simple_mvp.dart
... ... @@ -0,0 +1,12 @@
  1 +#library('simple_mvp.dart');
  2 +
  3 +#import("dart:html");
  4 +#import("dart:json");
  5 +
  6 +#source('src/model_attributes.dart');
  7 +#source('src/storage.dart');
  8 +#source('src/events.dart');
  9 +#source('src/model_list.dart');
  10 +#source('src/model.dart');
  11 +#source('src/delegated_event.dart');
  12 +#source('src/presenter.dart');
34 lib/src/delegated_event.dart
... ... @@ -0,0 +1,34 @@
  1 +/**
  2 +* Utility class implementing "jquery delegate" functionality.
  3 +*/
  4 +class _DelegatedEvent {
  5 + final _eventSelector, _callback;
  6 +
  7 + _DelegatedEvent(this._eventSelector, this._callback);
  8 +
  9 + void registerOn(HtmlElement parent) {
  10 + var parentCallback = _createCallbackOn(parent);
  11 + var eventList = parent.on[_eventType()];
  12 + eventList.add(parentCallback);
  13 + }
  14 +
  15 + _createCallbackOn(parent) => (event) {
  16 + if (_isTriggeredBySelector(parent, event)) {
  17 + _callback(event);
  18 + }
  19 + };
  20 +
  21 + _isTriggeredBySelector(parent, event) =>
  22 + _delimiter() == -1 ?
  23 + true :
  24 + parent.queryAll(_selector()).some((el) => el == event.target);
  25 +
  26 + _eventType() =>
  27 + _delimiter() == -1 ?
  28 + _eventSelector :
  29 + _eventSelector.substring(0, _delimiter());
  30 +
  31 + _selector() => _eventSelector.substring(_delimiter() + 1);
  32 +
  33 + _delimiter() => _eventSelector.indexOf(' ');
  34 +}
85 lib/src/events.dart
... ... @@ -0,0 +1,85 @@
  1 +typedef Listener(event);
  2 +
  3 +/**
  4 +* Utility class to manage event listeners.
  5 +*/
  6 +class Listeners {
  7 + final List listeners = [];
  8 +
  9 + Listeners add(Listener listener){
  10 + listeners.add(listener);
  11 + return this;
  12 + }
  13 +
  14 + void dispatch(event) {
  15 + listeners.forEach((fn) {fn(event);});
  16 + }
  17 +}
  18 +
  19 +/**
  20 +* Utility class to manage multiple event types.
  21 +*/
  22 +class EventMap {
  23 + final _listeners = new Map();
  24 +
  25 + Listeners listeners(String eventType){
  26 + _listeners.putIfAbsent(eventType, () => new Listeners());
  27 + return _listeners[eventType];
  28 + }
  29 +}
  30 +
  31 +/**
  32 +* Event map for the ModelList class.
  33 +*/
  34 +class CollectionEvents extends EventMap {
  35 + Listeners get load => listeners("load");
  36 + Listeners get insert => listeners("insert");
  37 + Listeners get remove => listeners("remove");
  38 + Listeners get update => listeners("update");
  39 +}
  40 +
  41 +/**
  42 +* An event that is raised after a collection's initial load.
  43 +*/
  44 +class CollectionLoadEvent {
  45 + final collection;
  46 +
  47 + CollectionLoadEvent(this.collection);
  48 +}
  49 +
  50 +/**
  51 +* Event raised after a new record was inserted into a collection.
  52 +*/
  53 +class CollectionInsertEvent {
  54 + final collection, model;
  55 +
  56 + CollectionInsertEvent(this.collection, this.model);
  57 +}
  58 +
  59 +/**
  60 +* Event raised after a record was removed from its collection.
  61 +*/
  62 +class CollectionRemoveEvent {
  63 + final collection, model;
  64 +
  65 + CollectionRemoveEvent(this.collection, this.model);
  66 +}
  67 +
  68 +/**
  69 +* Event map for the Model class.
  70 +*/
  71 +class ModelEvents extends EventMap {
  72 + Listeners get change => listeners("change");
  73 +}
  74 +
  75 +/**
  76 +* Event raised after a model's attribute was changed.
  77 +*/
  78 +class ChangeEvent {
  79 + final String attrName;
  80 + final model;
  81 + final oldValue;
  82 + final newValue;
  83 +
  84 + ChangeEvent(this.model, this.attrName, this.oldValue, this.newValue);
  85 +}
62 lib/src/model.dart
... ... @@ -0,0 +1,62 @@
  1 +/**
  2 +* The base class for model classes. It manages attribute changes, and persistence.
  3 +*/
  4 +abstract class Model {
  5 + Storage storage;
  6 + ModelAttributes attributes;
  7 + ModelList modelList;
  8 +
  9 + final ModelEvents on = new ModelEvents();
  10 +
  11 + Model(Map attributes, [this.modelList]){
  12 + this.attributes = new ModelAttributes(this, attributes);
  13 + storage = new Storage({
  14 + "read" : rootUrl,
  15 + "create" : createUrl,
  16 + "update" : updateUrl,
  17 + "destroy" : destroyUrl});
  18 + }
  19 +
  20 + /**
  21 + * Abstract property to be overriden by sublasses. It's used by Storage.
  22 + * By default [createUrl], [updateUrl], and [destroyUrl] are equal to [rootUrl].
  23 + */
  24 + String get rootUrl;
  25 + String get createUrl => rootUrl;
  26 + String get updateUrl => rootUrl;
  27 + String get destroyUrl => rootUrl;
  28 +
  29 + get id => attributes["id"];
  30 +
  31 + bool get saved => attributes.hasId();
  32 +
  33 + Future save(){
  34 + var f = saved ? storage.update(id, attributes) : storage.create(attributes);
  35 + f.then((attrs) => attributes.reset(attrs));
  36 + return f;
  37 + }
  38 +
  39 + Future destroy(){
  40 + if(modelList != null){
  41 + modelList.remove(this);
  42 + }
  43 + return storage.destroy(id);
  44 + }
  45 +
  46 + operator [] (String name) => attributes[name];
  47 +
  48 + operator []= (String name, value) => attributes[name] = value;
  49 +
  50 + noSuchMethod(String name, args){
  51 + if(name.startsWith("get:")){
  52 + return this[_extractAttributeName(name)];
  53 +
  54 + } else if (name.startsWith("set:")){
  55 + return this[_extractAttributeName(name)] = args[0];
  56 + }
  57 +
  58 + throw new NoSuchMethodError(this, name, args);
  59 + }
  60 +
  61 + _extractAttributeName(name) => name.substring(4);
  62 +}
46 lib/src/model_attributes.dart
... ... @@ -0,0 +1,46 @@
  1 +/**
  2 +* Exception raised when accessing a missing attribute.
  3 +*/
  4 +class InvalidAttributeError extends Error {
  5 + var model, name;
  6 + InvalidAttributeError(this.model, this.name);
  7 +}
  8 +
  9 +/**
  10 +* Utility class to manage a model's attributes.
  11 +*/
  12 +class ModelAttributes {
  13 + final Model model;
  14 + Map map;
  15 +
  16 + ModelAttributes(this.model, this.map);
  17 +
  18 + hasId() => map.containsKey("id");
  19 +
  20 + operator [] (String name){
  21 + if(! map.containsKey(name)){
  22 + throw new InvalidAttributeError(model, name);
  23 + }
  24 + return map[name];
  25 + }
  26 +
  27 + operator []= (String name, value){
  28 + if(! map.containsKey(name)){
  29 + throw new InvalidAttributeError(model, name);
  30 + }
  31 +
  32 + var oldValue = map[name];
  33 + map[name] = value;
  34 +
  35 + if(value != oldValue){
  36 + var event = new ChangeEvent(model, name, oldValue, value);
  37 + model.on.change.dispatch(event);
  38 + }
  39 + }
  40 +
  41 + void reset(Map attrs){
  42 + map = attrs;
  43 + }
  44 +
  45 + asJSON() => map;
  46 +}
47 lib/src/model_list.dart
... ... @@ -0,0 +1,47 @@
  1 +/**
  2 +* The base class for model lists.
  3 +*/
  4 +abstract class ModelList<T extends Model> {
  5 + final CollectionEvents on = new CollectionEvents();
  6 + final List<T> models = [];
  7 + Storage storage;
  8 +
  9 + ModelList(){
  10 + storage = new Storage({"readAll" : rootUrl});
  11 + }
  12 +
  13 + /**
  14 + * Abstract property to be overriden by sublasses. It's used by Storage.
  15 + */
  16 + String get rootUrl;
  17 +
  18 + T makeInstance(Map attrs, ModelList list);
  19 +
  20 + forEach(fn(T)) => models.forEach(fn);
  21 +
  22 + map(fn(T)) => models.map(fn);
  23 +
  24 + void add(T model){
  25 + models.add(model);
  26 + model.modelList = this;
  27 + on.insert.dispatch(new CollectionInsertEvent(this, model));
  28 + }
  29 +
  30 + void remove(T model){
  31 + var index = models.indexOf(model);
  32 + if(index == -1) return;
  33 +
  34 + models.removeRange(index, 1);
  35 + on.remove.dispatch(new CollectionRemoveEvent(this, model));
  36 + }
  37 +
  38 + void reset(List list){
  39 + models.clear();
  40 + models.addAll(list.map((attrs) => makeInstance(attrs, this)));
  41 + on.load.dispatch(new CollectionLoadEvent(this));
  42 + }
  43 +
  44 + void fetch(){
  45 + storage.readAll().then(reset);
  46 + }
  47 +}
35 lib/src/presenter.dart
... ... @@ -0,0 +1,35 @@
  1 +typedef String Template<T>(T model);
  2 +
  3 +/**
  4 +* It binds a model and view.
  5 +* Sends commands to the model when the associated view changes.
  6 +* Updates the view when the model changes.
  7 +*/
  8 +class Presenter<T> {
  9 + final HtmlElement el;
  10 + final Template<T> template;
  11 + final T model;
  12 +
  13 + Map get events => {};
  14 +
  15 + Presenter(this.model, this.el, [this.template]){
  16 + subscribeToModelEvents();
  17 + subscribeToDOMEvents();
  18 + }
  19 +
  20 + Presenter<T> render(){
  21 + if(template != null){
  22 + el.innerHTML = template(model);
  23 + }
  24 + return this;
  25 + }
  26 +
  27 + void subscribeToDOMEvents(){
  28 + events.forEach((eventSelector, callback){
  29 + new _DelegatedEvent(eventSelector, callback).registerOn(el);
  30 + });
  31 + }
  32 +
  33 + void subscribeToModelEvents(){
  34 + }
  35 +}
39 lib/src/storage.dart
... ... @@ -0,0 +1,39 @@
  1 +/**
  2 +* Utility class to read/update/delete models on the server.
  3 +*/
  4 +class Storage {
  5 + final _urls;
  6 +
  7 + Storage(this._urls);
  8 +
  9 + Future<List> readAll() => _submit("GET", _urls["readAll"], json: {});
  10 +
  11 + Future<Map> read(id) => _submit("GET", _urls["read"], id: id);
  12 +
  13 + Future<Map> create(ModelAttributes attrs) => _submit("POST", _urls["create"], json: attrs.asJSON());
  14 +
  15 + Future<Map> update(id, ModelAttributes attrs) => _submit("PUT", _urls["update"], id: id, json: attrs.asJSON());
  16 +
  17 + Future<Map> destroy(id) => _submit("DELETE", _urls["destroy"], id: id);
  18 +
  19 + _submit(method, url, {id, json}){
  20 + var c = new Completer();
  21 + url = id != null ? "$url/$id" : url;
  22 + var req = _createRequest(method, url, (res) => c.complete(res));
  23 + req.send(JSON.stringify(json));
  24 + return c.future;
  25 + }
  26 +
  27 + _createRequest(method, url, callback){
  28 + var req = new HttpRequest();
  29 +
  30 + req.on.load.add((e){
  31 + String response = req.response;
  32 + var parsedResponse = response.isEmpty() ? {} : JSON.parse(response);
  33 + callback(parsedResponse);
  34 + });
  35 +
  36 + req.open(method, url, true);
  37 + return req;
  38 + }
  39 +}
7 pubspec.yaml
... ... @@ -0,0 +1,7 @@
  1 +name: simple_mvp
  2 +version: 0.1.0
  3 +description: >
  4 + SimpleMVP is a framework for writing single-page applications in Dart.
  5 +dependencies:
  6 + unittest:
  7 + sdk: unittest
37 test/events_test.dart
... ... @@ -0,0 +1,37 @@
  1 +testEvents() {
  2 + group("events_test", () {
  3 + var listener1 = (e){};
  4 + var listener2 = (e){};
  5 + var capturer = new EventCapturer();
  6 +
  7 + group("Listeners", () {
  8 + test("adds listeners", () {
  9 + var l = new Listeners();
  10 + l.add(listener1).add(listener2);
  11 + expect(l.listeners, equals([listener1, listener2]));
  12 + });
  13 +
  14 + test("dispatches events", () {
  15 + var l = new Listeners();
  16 + l.add(capturer.callback);
  17 +
  18 + l.dispatch("expected event");
  19 +
  20 + expect(capturer.event, equals("expected event"));
  21 + });
  22 + });
  23 +
  24 + group("EventMap", () {
  25 + test("stores a list of listeners for the given event type", () {
  26 + var e = new EventMap();
  27 + e.listeners("type1").add(listener1);
  28 + expect(e.listeners("type1").listeners, equals([listener1]));
  29 + });
  30 +
  31 + test("creates a new list for every event type", () {
  32 + var e = new EventMap();
  33 + expect(e.listeners("type1"), isNot(equals(e.listeners("type2"))));
  34 + });
  35 + });
  36 + });
  37 +}
91 test/model_attributes_test.dart
... ... @@ -0,0 +1,91 @@
  1 +testModelAttributes() {
  2 + group("model_attributes_test", () {
  3 + var capturer;
  4 + var model;
  5 + var attrs;
  6 +
  7 + group("operator[]", () {
  8 + setUp(() {
  9 + model = new TestModel({});
  10 + attrs = new ModelAttributes(model, {"key": "value"});
  11 + capturer = new EventCapturer();
  12 + });
  13 +
  14 + test("returns the attribute's value", () {
  15 + expect(attrs["key"], equals("value"));
  16 + });
  17 +
  18 + test("raises an exception when invalid attribute name", () {
  19 + expect(() => attrs["invalid"], throws);
  20 + });
  21 + });
  22 +
  23 + group("operator[]=", () {
  24 + setUp(() {
  25 + model = new TestModel({});
  26 + attrs = new ModelAttributes(model, {"key": "value"});
  27 + capturer = new EventCapturer();
  28 + });
  29 +
  30 + test("updates the attribute's value", () {
  31 + attrs["key"] = "newValue";
  32 + expect(attrs["key"], equals("newValue"));
  33 + });
  34 +
  35 + test("raises an exception when invalid attribute name", () {
  36 + expect(() => attrs["invalid"] = "value", throws);
  37 + });
  38 +
  39 + test("raises an event when the value has changed", () {
  40 + model.on.change.add(capturer.callback);
  41 + attrs["key"] = "newValue";
  42 +
  43 + expect(capturer.event.attrName, equals("key"));
  44 + expect(capturer.event.oldValue, equals("value"));
  45 + expect(capturer.event.newValue, equals("newValue"));
  46 + });
  47 +
  48 + test("does not raise an event when the new value is the same", () {
  49 + model.on.change.add(capturer.callback);
  50 + attrs["key"] = attrs["key"];
  51 +
  52 + expect(capturer.event, isNull);
  53 + });
  54 + });
  55 +
  56 + group("hasId", (){
  57 + test("is true when attributes containt id", (){
  58 + attrs = new ModelAttributes(null, {"id": "value"});
  59 + expect(attrs.hasId(), isTrue);
  60 + });
  61 +
  62 + test("is false otherwise", (){
  63 + attrs = new ModelAttributes(null, {});
  64 + expect(attrs.hasId(), isFalse);
  65 + });
  66 + });
  67 +
  68 + group("reset", (){
  69 + setUp(() {
  70 + model = new TestModel({});
  71 + attrs = new ModelAttributes(model, {"key": "value"});
  72 + capturer = new EventCapturer();
  73 + });
  74 +
  75 + test("updates existing attribitues", (){
  76 + attrs.reset({"key": "newValue"});
  77 + expect(attrs["key"], equals("newValue"));
  78 + });
  79 +
  80 + test("creates new attributes", (){
  81 + attrs.reset({"newKey": "newValue"});
  82 + expect(attrs["newKey"], equals("newValue"));
  83 + });
  84 +
  85 + test("removes attributes", (){
  86 + attrs.reset({});
  87 + expect(() => attrs["key"], throws);
  88 + });
  89 + });
  90 + });
  91 +}
57 test/model_list_test.dart
... ... @@ -0,0 +1,57 @@
  1 +testModelLists() {
  2 +
  3 + group("model_list_test", () {
  4 + var capturer = new EventCapturer();
  5 + var list;
  6 + var model;
  7 +
  8 + group("add", () {
  9 + setUp(() {
  10 + list = new TestModelList();
  11 + model = new TestModel({});
  12 + });
  13 +
  14 + test("adds the element to the collection", () {
  15 + list.add(model);
  16 + expect(list.models, equals([model]));
  17 + });
  18 +
  19 + test("sets the modelList property on the model", () {
  20 + list.add(model);
  21 + expect(model.modelList, equals(list));
  22 + });
  23 +
  24 + test("raises an event", () {
  25 + list.on.insert.add(capturer.callback);
  26 + list.add(model);
  27 +
  28 + expect(capturer.event.model, equals(model));
  29 + });
  30 + });
  31 +
  32 + group("remove", () {
  33 + setUp(() {
  34 + list = new TestModelList();
  35 + model = new TestModel({});
  36 + list.add(model);
  37 + });
  38 +
  39 + test("removes the element from the collection", () {
  40 + list.remove(model);
  41 + expect(list.models, equals([]));
  42 + });
  43 +
  44 + test("does nothing when cannot find the element", () {
  45 + list.remove(model);
  46 + list.remove(model);
  47 + });
  48 +
  49 + test("raises an event", () {
  50 + list.on.insert.add(capturer.callback);
  51 + list.add(model);
  52 +
  53 + expect(capturer.event.model, equals(model));
  54 + });
  55 + });
  56 + });
  57 +}
99 test/model_test.dart
... ... @@ -0,0 +1,99 @@
  1 +testModels() {
  2 + group("model_test", () {
  3 + var capturer;
  4 + var model;
  5 +
  6 + setUp(() {
  7 + capturer = new EventCapturer();
  8 + });
  9 +
  10 + group("saved", () {
  11 + test("is true when id is set", () {
  12 + model = new TestModel({"id": 1});
  13 + expect(model.saved, isTrue);
  14 + });
  15 +
  16 + test("is false otherwise", () {
  17 + model = new TestModel({});
  18 + expect(model.saved, isFalse);
  19 + });
  20 + });
  21 +
  22 + group("attributes", () {
  23 + setUp(() {
  24 + model = new TestModel({"key": "value"});
  25 + });
  26 +
  27 + test("returns the attribute's value", () {
  28 + expect(model.key, equals("value"));
  29 + });
  30 +
  31 + test("updates the attribute's value", () {
  32 + model.key = "newValue";
  33 + expect(model.key, equals("newValue"));
  34 + });
  35 +
  36 + test("supports map syntax", () {
  37 + model["key"] = "newValue";
  38 + expect(model["key"], equals("newValue"));
  39 + });
  40 + });
  41 +
  42 + group("save", () {
  43 + var dummyFuture = new Future.immediate({"key": "newValue"});
  44 +
  45 + test("creates the element in the storage", () {
  46 + model = new TestModel({"key": "value"});
  47 + model.storage.when(callsTo('create', model.attributes)).alwaysReturn(dummyFuture);
  48 + model.save();
  49 + });
  50 +
  51 + test("updates the element in the storage", () {
  52 + model = new TestModel({"id": 1, "key": "value"});
  53 + model.storage.when(callsTo('update', 1, model.attributes)).alwaysReturn(dummyFuture);
  54 + model.save();
  55 + });
  56 +
  57 + test("resets the model's attributes after receiving a response from the server", () {
  58 + model = new TestModel({"key": "value"});
  59 + model.storage.when(callsTo('create')).alwaysReturn(dummyFuture);
  60 + model.save();
  61 + expect(model.key, equals("newValue"));
  62 + });
  63 +
  64 + test("returns a future", () {
  65 + model = new TestModel({"key": "value"});
  66 + model.storage.when(callsTo('create')).alwaysReturn(dummyFuture);
  67 + var future = model.save();
  68 + expect(future, equals(dummyFuture));
  69 + });
  70 + });
  71 +
  72 + group("destroy", () {
  73 + var list;
  74 + var dummyFuture = new Future.immediate("dummy");
  75 +
  76 + setUp(() {
  77 + model = new TestModel({"id": 1});
  78 + list = new TestModelList();
  79 + });
  80 +
  81 + test("removes the element from the storage", () {
  82 + model.destroy();
  83 + model.storage.getLogs(callsTo('destroy', 1)).verify(happenedExactly(1));
  84 + });
  85 +
  86 + test("returns a future", () {
  87 + model.storage.when(callsTo('destroy')).alwaysReturn(dummyFuture);
  88 + var future = model.destroy();
  89 + expect(future, equals(dummyFuture));
  90 + });
  91 +
  92 + test("removes itself from the model list", () {
  93 + list.add(model);
  94 + model.destroy();
  95 + expect(list.models, equals([]));
  96 + });
  97 + });
  98 + });
  99 +}
13 test/run.html
... ... @@ -0,0 +1,13 @@
  1 +<!DOCTYPE html>
  2 +
  3 +<html>
  4 +<head>
  5 + <meta charset="utf-8">
  6 + <title>run_tests</title>
  7 +</head>
  8 +<body>
  9 +<h1>Tests</h1>
  10 +<script type="application/dart" src="simple_mvp_test.dart"></script>
  11 +<script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script>
  12 +</body>
  13 +</html>
22 test/simple_mvp_test.dart
... ... @@ -0,0 +1,22 @@
  1 +#library("simple_mvp_test");
  2 +
  3 +#import("package:unittest/unittest.dart");
  4 +#import("package:unittest/html_enhanced_config.dart");
  5 +#import('../lib/simple_mvp.dart');
  6 +
  7 +#import('dart:html');
  8 +
  9 +#source("utils.dart");
  10 +#source("events_test.dart");
  11 +#source("model_attributes_test.dart");
  12 +#source("model_test.dart");
  13 +#source("model_list_test.dart");
  14 +
  15 +main(){
  16 + useHtmlEnhancedConfiguration();
  17 +
  18 + testModels();
  19 + testEvents();
  20 + testModelAttributes();
  21 + testModelLists();
  22 +}
26 test/utils.dart
... ... @@ -0,0 +1,26 @@
  1 +class EventCapturer {
  2 + var event;
  3 +
  4 + callback(e){
  5 + event = e;
  6 + }
  7 +}
  8 +
  9 +class MockStorage extends Mock implements Storage {}
  10 +
  11 +class TestModel extends Model {
  12 + TestModel(attrs, [list]): super(attrs, list) {
  13 + storage = new MockStorage();
  14 + }
  15 + final rootUrl = "url";
  16 +}
  17 +
  18 +class TestModelList extends ModelList<TestModel> {
  19 + TestModelList(){
  20 + storage = new MockStorage();
  21 + }
  22 +
  23 + final rootUrl = "url";
  24 +
  25 + makeInstance(attrs, list) => new TestModel(attrs, list);
  26 +}

0 comments on commit 22c0fba

Please sign in to comment.
Something went wrong with that request. Please try again.