MVVM, REST Json Service, Message Subscriptions for Web Atoms
npm install web-atoms-mvvmWeb Atoms
Web Atoms Unit
typescript
var BaseService = WebAtoms.BaseService;
@DIGlobal
class BackendService extends BaseService{
@Get("/task/{taskId}")
getTask(
@Path("taskId") taskId:number
): Promise{
return null;
}
@Put("/task/{taskId}")
saveTask(
@Path("taskId") taskId: number,
@Body task:Task
):Promise{
return null;
}
@Delete("/task/{taskId}")
deleteTask(
@Path("taskId") taskId: number
):Promise{
return null;
}
// support for CancelToken
@Get("/tasks")
getTasks(
@Query("deleted" deleted: boolean,
@Cancel cancel:CancelToken
):Promise>{
return null;
}
}
`
Sample View Model
`typescript
var AtomViewModel = WebAtoms.AtomViewModel;
var AtomList = WebAtoms.AtomList;
var AtomCommand = WebAtoms.AtomCommand;
class Task{
id: number;
label: string;
status: string;
}
class TaskViewModelErrors extends AtomErrors{
@bindableProperty
label: string;
@bindableProperty
status: string;
}
class TaskViewModel extends AtomViewModel{
@bindableProperty
list:AtomList;
@bindableProperty
newItem:Task;
@bindableProperty
selectedItem: Task;
addCommand: AtomCommand;
removeCommand: AtomCommand
backendService: BackendService;
errors:TaskViewModelErrors;
constructor(){
this.list = new AtomList();
this.newItem = new Task();
this.addCommand = new AtomCommand(a => onAddCommand());
this.removeCommand = new AtomCommand(task =>
onRemoveCommand(task);)
// simple dependency injection !!!
this.backendService = WebAtoms.DI.resolve(BackendService);
// setup validation
this.errors = new TaskViewModelErrors();
this.addValidation(
() => this.errors.label = this.newItem.label ? "" : "Task cannot be empty",
() => this.errors.status = this.newItem.status ? "" : "Status cannot be empty"
);
}
async init():Promise{
var results = await this.backendService.getTasks(false);
this.list.addAll(results);
}
onPropertyChanged(name:string){
// called when any property of this viewmodel
// is modified anywhere
}
async addCommand():Promise{
var windowService = WebAtoms.DI.resolve(WindowService);
if(this.errors.hasErrors()){
windowService.alert("Please complete all required fields");
return;
}
this.newItem =
await this.backendService.saveTask(this.newItem);
this.list.add(this.newItem);
this.newItem = new Task();
}
async removeCommand(task:Task):Promise{
await this.backendService.deleteTask(task.id);
this.list.remove(task);
}
}
`
Sample HTML
`html
atom-type="AtomApplication"
atom-view-model="{ new TaskViewModel() }">
atom-value="$[viewModel.newItem.label]"
placeholder="New Task"/>
atom-type="AtomButton"
atom-command="{$viewModel.addCommand}">Add Mew
atom-type="AtomListBox"
atom-items="[$viewModel.list]">
atom-type="AtomDeleteButton"
atom-command="{$viewModel.removeCommand}"
atom-command-parameter="{$data}">Delete
Sample Broadcast Listener
Let's assume, you have a Atom Component that displays notifications.
And you have set atom-view-model to NotificationServiceViewModel
`typescript
class AtomNotification {
static short(message: string, title: string = ""): AtomNotification {
return new AtomNotification(message, title);
}
constructor(
message: string,
title: string,
icon: string = null,
delay: number = 5000)
{
this.message = message;
this.title = title;
this.icon = icon || "";
this.delay = delay || 5000;
}
message: string;
title: string;
// displayed on left side..
icon: string;
// maximum delay to be displayed
@bindableProperty
delay: number;
timeout: number;
}
class NotificationServiceViewModel extends AtomViewModel {
notifications: AtomList;
removeCommand: AtomCommand;
constructor() {
super();
this.notifications = new AtomList();
this.removeCommand = new AtomCommand (n => this.onRemoveCommand(n));
}
@receive("ui-notification")
uiNotification(channel:string, data: AtomNotification){
this.onNotified(data);
}
async onRemoveCommand(n: AtomNotification): Promise {
this.notifications.remove(n);
if (n.timeout) {
clearTimeout(n.timeout);
}
}
async onNotified(n: AtomNotification): Promise {
this.notifications.add(n);
if (n.delay > 0) {
this.updateTimer(n);
}
}
// this will reduce delay by 1000 millseconds
// you can bind n.delay to display "Closing in (n) seconds" etc
private updateTimer(n: AtomNotification): void {
if (n.delay > 0) {
n.delay = n.delay - 1000;
setTimeout(() => this.updateTimer(n), 1000);
} else {
this.notifications.remove(n);
}
}
}
`
Sample Notification UI
`html
atom-type="AtomItemsControl"
atom-view-model="{ new NotificationServiceViewModel() }"
atom-items="{$viewModel.notifications}"
style="position:absolute;top:0;left:0;"
style-display="[$viewModel.notifications.length ? 'block' : 'none']"
style-width="100%"
style-height="100%">
atom-presenter="itemsPresenter">
atom-type="AtomButton"
atom-command-parameter="{$data}"
atom-command="{$viewModel.removeCommand}"
>Close
Reference
AtomDisposable
`typescript
interface AtomDisposable{
dispose();
}
// action f will be called on dispose
class DisposableAction implements AtomDisposable{
constructor(f:()=>void);
}
`
Atom
`typescript
class Atom{
// watch for change in property in target object
// method will return subscription, you can call dispose to
// remove subscription to avoid memory leak
static watch(target:any, property:string, f:()=>void) : AtomDisposable;
static async delay(n:number, ct?:CancelToken):Promise;
}
`
AtomCommand
Although you can directly call viewModel methods into binding expressions,
we recommend using AtomCommand, as command has busy property, which will be set
to true when asynchronous Promise is still executing.
You can set enabled to false to disable any UI associated with it, AtomButton automatically
binds to enabled property.
`typescript
class AtomCommand{
@bindableProperty
enabled: boolean;
@bindableProperty
busy: boolean;
constructor( action:(T) => any );
}
`
AtomModel
`typescript
class AtomModel{
// this will notify bindings to refresh the UI
refresh(field:string);
}
`
AtomViewModel
`typescript
class AtomViewModel extends AtomModel{
constructor();
async init();
dispose();
broadcast(message:string,data:any);
addValidation(...f:(()=>any)[]);
// you can register a disposable which will
// be disposed when View Model will be disposed by the UI service
registerDisposable(d:AtomDisposable);
// called when any property of view model
// is changed
onPropertyChanged(name:string);
// decorators for AtomViewModel
// @receive(...string[]) sets up receiver for broadcast for given channels
// @watch sets up watching and evaluating expressions when any of property gets modified
// @validate sets up validation (same as addValidation)
}
`
AtomDevice
`typescript
type AtomAction = (msg: string, data: any) => void;
class AtomDevice{
// only access static instance
// never create an instance
static instance:AtomDevice;
broadcast(message:string, data:any);
// method will return subscription
// you must call subscription.dispose() to avoid memory leak
subscribe(message:string, action:AtomAction ): AtomDisposable;
// takes care of errors
runAsync(task:Promise):Promise;
}
`
AtomList
`typescript
class AtomList{
add(item:T):number;
addAll(items:Array);
insert(i: number, item: T);
removeAt(i: number);
remove(item: T| (t:T) => bool);
clear();
// refreshes binding
refresh();
// watch for changes in events..
// type: (add/remove)
// returns AtomDisposable
// you must call dispose on AtomDisposable to avoid
// memory leak
watch(f:(type:string, index:number)=>void): AtomDisposable;
}
`
DI
`typescript
class DI{
// this will resolve instance
// by providing type
// example,
//
// @DIGlobal
// class BackendService{
// ....
// }
//
// var servie = WebAtoms.DI.resolve(BackendService);
static resolve(c:new () => T):T;
// register a type in code instead of @DIGlobal
// example,
//
// WebAtoms.DI.register(BackendService, () => new BackendService() );
static register(c:new ()=>T, factory: ()=> T);
// manually override global instance, if you want to override factory, you can
// call register method
// example,
//
// WebAtoms.DI.override(BackendService, new MockBackendService());
//
static override(c:new () => T, instance:T);
}
`
Window Service
`typescript
@DIGlobal
class WindowService{
async confirm(msg:string, title?:string):Promise
async alert(msg:string, title?:string): Promise
async openWindow( windowType: (string | new() => AtomWindow), viewModel?: AtomViewModel ): Promise
}
`
Web Atoms Custom Controls (Custom Components)
You can create sample component as shown below and it will generate JavaScript which you can use as custom control.
As dividing html into smaller fragments is pain, instead, small individual components can be scattered around in
folders for better management and component generator will generate single JavaScript which can be reused on pages.
Component supports style with css/less tags.
`html
class="task-list"
atom-component="TaskList"
atom-view-model="{ new TaskListViewModel() }">
atom-type="AtomDeleteButton"
atom-next="{ => $viewModel.deleteTask($data) }">
Component Generator
`node
node node_modules/web-atoms-mvvm/bin/component-generator.js
Example,
node node_modules/web-atoms-mvvm/bin/component-generator.js app
`
waconfig.json
Component generator will lookup waconfig.json and will generate files accordingly. Sample waconfig.json
`json
{
"srcFolder": "src/views",
"outFile": "build/views.js",
"namespace": ".. (namespace is optional) .."
}
`
This will generate component.js for all html files app folder.
Usage
Once component is generated, you can include generated javascript file and create control in your main page as shown below.
`html
`
Component Generator Task Example
Following is an example of how to add custom component generator task in VS Code.
`json
{
"taskName": "Component Compiler",
"command": "node",
"isBackground": true,
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated"
},
"args": [
"node_modules/web-atoms-mvvm/bin/component-generator.js",
"app"
],
"problemMatcher":{
"owner": "cc",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": {
"regexp": "^([^\\(].)\\((\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s:\\s(.)$",
"file": 1,
"location": 2,
"severity": 3,
"code": 4,
"message": 5
},
"background": {
"activeOnStart": true,
"beginsPattern": "^\\s*\\d{1,2}:\\d{1,2}:\\d{1,2}(?: AM| PM)? - File change detected\\. Starting incremental compilation\\.\\.\\.",
"endsPattern": "^\\s*\\d{1,2}:\\d{1,2}:\\d{1,2}(?: AM| PM)? - Compilation complete\\. Watching for file changes\\."
}
}
}
``