Introduction
What is web atoms?
"Web Atoms" is an advanced MVVM framework to write cross platform applications in HTML5
and Xamarin.Forms
. Unlike other frameworks, Web Atoms lets you divide User Interface logic in strict MVVM fashion and separates View in HTML5
and Xaml
. Benefit of separating User interface logic in ViewModel is you can individually unit test view model to make sure your logic is consistent across platforms.
Also everything is transpiled into JavaScript, your View Model and Services remain in JavaScript and in browser it works flawlessly.
In Xamarin.Forms, Web Atoms package written C# helps you easily host JavaScript modules inside an application and entire User Interface is hosed via JavaScript.
Benefits of Web Atoms with Xamarin.Forms
- Small application download size
- Even Xaml views are converted to JavaScript
- Reuse existing NuGet components by exposing via services
- Host javascript on server with instant updates to apps
- No native compilation needed unless you add/modify native services written in c#
- You can use Xaml binding as well as Web Atoms's JavaScript bindings
Requirements
- VS Code
- TSLint extension
- NodeJS with NPM
- Visual Studio for Xamarin.Forms app [optional]
- Gitlense [optional]
UMD Loader
All web atoms modules are written (transpiled) as UMD module, suitable for testing in node as well as to load in browser with AMD Loader.
<html>
<head>
<title>Web Atoms Core Samples</title>
<!-- AMD Loader -->
<script type="text/javascript" src="./node_modules/web-atoms-amd-loader/umd.js"></script>
</head>
<body>
<script type="text/javascript">
// map every package and its relative or absolute
// path (from cdn)
UMD.map("reflect-metadata", "./node_modules/reflect-metadata/Reflect.js");
UMD.map("web-atoms-core", "./node_modules/web-atoms-core");
UMD.map("web-atoms-samples", "./");
// set language
UMD.lang = "en-US";
// Load view in entire page, this method will
// resolve package from the above mentioned map of
// package to url. And it will create an instance of
// App class and it will host the view `AppHost` in the
// body of this document
UMD.loadView("web-atoms-samples/dist/web/views/AppHost", true);
</script>
</body>
</html>
Web Atoms Generator
Each html file under src
folder is transpiled to a TypeScript file that can contains a class derived from AtomControl. This class is a view that can be loaded with UMD.loadView
method and it can be nested inside any other view.
Similarly, each xaml file under src
folder is transpiled to a TypeScript file, that can be used inside Xamarin.Forms application.
Please refer to generator for how to setup.
Directory structure
root
+ dist // dynamically generated by compilation, ignored in git
...
+ src
+ model // all models here
+ services // all services inside this folder
+ tests // all unit tests
+ view-models // all view models must be placed here
+ web // all html files must be placed inside web folder
+ xf // all xaml files must be placed inside xf folder
It is important that you keep files inside web
and xf
folder, as module loader will replace {platform}
variable in url to corresponding folder to load views. This will make view models completely independent of platform.
AtomControl
AtomControl is a UI control which contains logic to render visual elements on the screen. AtomControl has an initialization lifecycle that is common in every platform. However, rendering lifecycle differs on every platform. For example, Xamarin.Forms
has its own render lifecycle so AtomControl only does binding to properties. In Web Browser
, it has special lifecycle to render contents. Most of the time developer does not need to worry about it as controls are created with best performance in mind.
Following properties exist on AtomControl and they are Logically Inherited.
Properties
- app (readonly)
- parent (readonly)
- data
- viewModel
- localViewModel
Why data and viewModel properties are separate?
In most of UI framework, view model is usually set in data
property, which leads to problems in multiple items control such as list box etc. That requires unnecessary climbing up hierarchy and get instance of parent's data property to bind. So having separate inherited viewModel
property makes it easier to reference viewModel associated with whole page or fragment.
What is localViewModel ?
To make reusable components easier, localViewModel
can be used to host all logic that is only specific to the component. For example, lets say you want to create a calendar component. Local view model will contain all the logic to create list of all dates for currently displayed month and year. It will also create list of all years. All this logic will be independent of rendering logic and it can be put inside a view model which can be unit tested separately. Benefit here is, you can write a reusable view model for platform dependent component that has common logic across different platforms. So inside component, you will only write binding expressions to localViewModel
.
AtomViewModel
AtomViewModel
class provides necessary services and properties to write easily extensible view models. It has init
and dispose
methods to initialize and dispose your view model.
public class TaskListViewModel extends AtomViewModel {
// dynamically inject TaskService
@Inject
public taskService: TaskService;
public tasks: ITaskModel[];
public async init(): Promise<void> {
this.tasks = await this.taskService.loadTasks();
}
}
Watch
Since view model does not have access to user interaction updates, View usually does two way binding to a property and when it is modified by user, we have to watch for changes and update view model. It is done via @Watch
decorator.
public class TaskListViewModel extends AtomViewModel {
@Inject
public taskService: TaskService;
public tasks: ITaskModel[];
public search: string = null;
public range = { start: 0, size: 10 };
public async init(): Promise<void> {
this.loadTasks(null);
}
// whenever any of search, range.start or range.size
// property is modified
// automatically call this method
@Watch
public watchSearch(): void {
this.loadTasks(
this.search, this.range.start, this.range.size );
}
private async loadTasks(
search: string,
start: number,
size: number
): Promise<void> {
this.tasks =
await this.taskService.loadTasks(search, start, size);
}
}
By convention, @Watch
decorated method must be prefixed with watch
word. Even if it is named differently, @Watch
decorator will still watch for changes.
Watch decorator automatically starts watching every expression that starts with this.
, it ignores methods and it watches only properties. For performance, this decorator does not parse javascript code, it only looks for this.identifier.identifier...
expression and creates map of watching every single property in entire expression.
Every property accessed inside
@Watch
decorated method must be initialized tonon undefined
value. Since binding framework ignoresundefined
Watch property
Watch can also be defined on a readonly property.
@Watch
public get fullName(): string {
return `${ this.model.firstName } ${ this.model.lastName }`;
}
HTML
<span text="[$viewModel.fullName]"></span>
XAML
<Label Text="[$viewModel.firstName]">
You can bind any view property to fullName
and it will refresh automatically whenever any changes was detected in model.firstName
or model.lastName
. Again, both must not initialized to undefined
.
Validate property
Though @Watch
is great way to watch any property, we cannot use it for validation because as soon as page is loaded, user will be thrown with error messages. So we have created @Validate
decorator which only returns an error message after this.isValid
property is called.
For example,
public class SignupViewModel extends AtomViewModel {
@Inject
public navigationService: NavigationService;
public model = {
firstName: null,
lastName: null
};
// both validate properties will return undefined value
// unless `this.isValid` is referenced.
@Validate
public get errorFirstName(): string {
return this.model.firstName ? "" : "First name is required";
}
@Validate
public get errorLastName(): string {
return this.model.firstName ? "" : "Last name is required";
}
public signup(): Promise<void> {
// as soon as this property is called first time
// validation decorator will update and error will be displayed
if (!this.isValid) {
await this.navigationService.alert(`Please enter required fields`);
return;
}
}
}
HTML
<div view-model="{ this.resolve(SignupViewModel) }">
<input placeholder="First name:" value="$[viewModel.model.firstName]">
<span class="error" text="[$viewModel.errorFirstName]"></span>
<input placeholder="Last name:" value="$[viewModel.model.lastName]">
<span class="error" text="[$viewModel.errorLastName]"></span>
...
<button event-click="{ () => $viewModel.signup() }">Signup</button>
</div>
XAML
<Entry
Placeholder="First name:"
Text="$[viewModel.model.firstName]"/>
<Label
Style="Error"
Text="[$viewModel.errorFirstName]"/>
<Entry
Placeholder="Last name:"
Text="$[viewModel.model.lastName]"/>
<Label
Style="Error"
Text="[$viewModel.errorLastName]"/>
...
<Button Command="{ () => $viewModel.signup() }">Signup</Button>
In above example, when page is loaded, error spans will not display anything. Even if firstName
and lastName
both are empty. As soon as user clicks Signup
button, this.isValid
get method will start watching for changes in all @Validate
decorator methods and user interface will start displaying error message.
Dive into samples
https://www.webatoms.in/samples.html#contextId=0
web-atoms / core
Light weight feature rich UI Framework for JavaScript for Browser with Dependency Injection, Mocking and Unit Testing
Web-Atoms Core
Lightweight JavaScript framework with MVU Pattern with Data Binding in JSX.
Note, MVVM is now deprecated, we have realized that MVVM often adds more code then the benefits. Since JavaScript allows mixin, its easy to incorporate reusable logic with mixin rather than MVVM. MVU pattern is better suitable for faster development.
Web Features
- Data Binding, simple arrow functions to bind the UI elements
- Styled Support
- AtomRepeater - Lightweight List Control to manage list of items
- Chips control
- Dual View support (Mobile and Desktop)
- Smallest syntax
- Faster rendering
- Simple Data Validations
- RetroFit inspired REST API Support
- No additional build configurations
- Event re routing, it helps in reducing number of event listeners on page.
- UMD and SystemJS Module Loader
- Packer, to pack all JavaScript in single module along with dynamic module loader support
- FetchBuilder, fetch builder allows you to build REST request in fluent way and execute them with single…
Top comments (0)