DEV Community

Akash Kava for Web Atoms

Posted on • Updated on

Introduction to Web Atoms Core

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

  1. VS Code
  2. TSLint extension
  3. NodeJS with NPM
  4. Visual Studio for Xamarin.Forms app [optional]
  5. 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 to non undefined value. Since binding framework ignores undefined

Watch property

Watch can also be defined on a readonly property.


    @Watch
    public get fullName(): string {
        return `${ this.model.firstName } ${ this.model.lastName }`;
    }

Enter fullscreen mode Exit fullscreen mode

HTML

    <span text="[$viewModel.fullName]"></span>
Enter fullscreen mode Exit fullscreen mode

XAML

    <Label Text="[$viewModel.firstName]">
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }

}

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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

GitHub logo web-atoms / core

MVVM Framework for JavaScript for Browser, Xamarin.Forms, Write TSX/TypeScript instead of Xaml and C#, Hot Reload Live Published Xamarin.Forms Apps.

Action Status npm version codecov

Web-Atoms Core

Web Atoms Core is a UI abstraction framework along with powerful MVVM pattern to design modern web and mobile applications.

Xamarin.Forms Features

  1. Use VS Code to develop Xamarin.Forms
  2. Write TypeScript instead of C#
  3. Write TSX (JSX) instead of Xaml
  4. Live hot reload for published app

Web Features

  1. Abstract Atom Component
  2. Abstract Device API (Browser Service, Message Broadcast)
  3. Theme and styles support without CSS
  4. One time, One way and Two way binding support
  5. Simple dependency Injection
  6. In built simple unit testing framework
  7. UMD module support
  8. Full featured MVVM Framework with powerful validation

Folder structure

  1. All views for web must be placed under "web" folder inside "src" folder.
  2. All views for Xamarin Forms must be placed under "xf" folder inside "src" folder.

Example folder structure

src
+--images
|  +--AddButton.svg
|
+--view-Models
|  +--TaskListViewModel.ts
|  +--TaskEditorViewModel.ts
|
+--web
|  +--tasks
|     +--TaskListView.tsx
|     +--TaskEditorView.tsx
|
+--xf
   +--tasks
      +--TaskListView.tsx
      +--TaskEditorView.tsx

Example View

Discussion (0)