Return to docs home page

View Models and Views

The key technology used for the presentation parts of Peanut is the KnockoutJS, a lightweight JavaScript library that supports two-way databinding between a JavaScript object called a ViewModel and a section of HTML called the View.

The online documentation provides a clear and easy explanation so I will refer you there rather than attempt an explanation here. Check out this like for a general understanding of viewmmodels, views, obsrvables and components. Then the rest of this article will make sense.

Knockout JS Documentaton

As long as you understand the concepts, you won't have to be concerned with the details of ViewModel loading and binding. You will need to discover how the various databinding work but you can pick that up as needed with the help of the Knockout online reference.

You will need to understand the conventions we use for creating and managing viewmodels, views and components. This is what I'll explain here.

ViewModels and Views in Peanut

Note: For the fist part of these instructions, please refer the the "Simple Test" page example. This is a "Hello World" type of basic demonstration of Peanut/Knockout.

You may not need to build your own, but understanding how it is done will help maintain the existing collection. It is important to follow our conventions for naming a file location since much of the functionality is driven by convention. Mind the case of filenames remembering that on the Linux servers where our sites live the file system is case sensitive.

Our viewmodels are written in TypeScript. If you are not familiar see (TypeScriptLang.org)[https://www.typescriptlang.org/]. Most details of our code is plain JavaScript, so, you know JavaScript you should be at home.

Views for us are fragments of HTML that are inserted in the page. These files are location in particular places depending on how you intend to apply or distrubute them.

Other Essentials

Knockout Components

These components are essentially smaller viewmodel/view pieces that can be used across view models. Components are loaded and registered in the ViewModel.init() method and referenced in the view as a custom tag.

To learn about how they are implemented in Peanut, see Components Overview

Service Commands

Service commands are PHP classes that recieve remote requests (Ajax) from a JavaScript/TypeScript client, usually a view model.

For a detailed discussion of Service Commands see: Service Commands

Location and naming conventions

Viewmodels are stored in direcories named "vm" with the related view file in a directory named "view" on the same directory level. We frequently use the term "viewmodel" to refer to both. The pair share a view model name used to name the files and to identify them in the code.

In our "Simple Test" example, convertions for names and locations of files, as well as naming of identifiers is as follows:

Creating and Installing a Viewmodel

The steps involved in producing a new viewmodel include:

  1. Choose a location for the viewmodel files
  2. Choose a globally unique view model name.
  3. Add an entry to a viewmodels.ini file
  4. Create and implement the viewmodel class and view html file.
  5. Implement any related service commands you will need. See: Service Commands
  6. Add a Knockout View block to a page as described in the previous section.

Locations

These are the various locations you can choose from. You can also put your files in a sub-directory if you indicate this in the viewmodel.ini entry.

Viewmodel names must be globally unique. For example if you have a one named "ContactForm" and there is an existing one by the same name, the loader will pick the first one it comes to when scanning the viewmodels.ini files. This is not fatal but could cause confusion. The easiest way to determine uniqueness is to use a search feature, such as the one in the PhpStorm IDE.

Entry in viewmodels.ini

The viewmodels.ini entry begins with "[viewmodel-identifier]" follow by vm=ViewModelName. Example:

[committees]
vm=Committees

If the viewmodel is located in a sub-directory, this can be included in the vm setting.

[simple-test]
vm=tests/SimpleTest

Other optional settings: ####

To start, open the template file, web.root/packages/knockout_view/pnut/examples/view-template.txt.

Replace the two occurance of "viewname" with the viewmodel name, the name not the identifier. For example the SimpleTest viewmodel will have two divs

<div id="simpletest-load-message"><span class="..." style="color:lightgrey"></span></div>
<div id="simpletest-view-container" style="display:none" class="..." >

Save in your chosen location as (ViewModel name).html.
Example: web.root/application/peanut/tests/view/SimpleTest.html

Now you can add your markup and data bindins inside the "-view-container" div.

Defining the ViewModel class in TypeScript

To start, open the template file, web.root/packages/knockout_view/pnut/examples/viewmodel-template.txt

Replace "VmNameViewModel" with the class name of your view model. E.g "SimpleTestViewModel"

Replace "PackageName" with the appropriate name space for your view model. For viewmodels in packages this corresponds to the directory name. Viewmodels in pnut/packages/qnut-directory have the namespace QnutDirectory. Viewmodels in application/peanut use "Peanut" as the namespace.

Save the file to your chosen location as (Your ViewModel name) + "ViewModel.ts".
Example: web.root/application/peanut/tests/vm/SimpleTestViewModel.ts

Depending on the location you may need to correct the relative paths in reference path elements.

If your location is web.root/application/peanut/tests/vm. The reference to ViewModelBase.ts is

/// <reference path='../../../../packages/knockout_view/pnut/core/ViewModelBase.ts' />

In web.root/application/peanut/vm.

/// <reference path='../../../packages/knockout_view/pnut/core/ViewModelBase.ts' />

In a package directory, e.g. web.root/packages/knockout_view/pnut/packages/qnut-directory/vm

/// <reference path="../../../../pnut/core/ViewModelBase.ts" />

File names are case sensitive. Correct:

/// <reference path='../../../../pnut/core/Peanut.d.ts' />

Incorrect:

/// <reference path='../../../../pnut/core/peanut.d.ts' />

If you have recently installed a type library using NPM you may not need to use a reference path. Just see if the TypeScript compiler or your IDE recognize the identifiers you use.

Now you can begin to implement the view model class. Start with defining knockout observables to match any databinding you put in the view. You don't have to initialize them yet but if you are testing any data-bind attribute in the view must match an observable in the viewmodel class or an error will be raised.

Example:

In SimpleTest.html:

<h2 data-bind="text:messageText">Not bound</h2>

Must have a corresponding observable defined in SimpleTestViewModel.ts

    export class SimpleTestViewModel  extends Peanut.ViewModelBase {
    messageText =
        ko.observable('This is a simple test, just to make sure all the foundational MVVM stuff is working.');

}

The next step is to implement the init() method. Here you will load any resources, libraries, components, styles and execute any Service Commands you need to download initialization data.

The ViewModel.init() function

The init() function is where you implement any custom code that must run after all other page elements are loaded and before the view is displayed. This can include variable and observable initialization and loading of any additional JavaScript libraries, style sheets and other resources that your view model depends on.

Often there will be a call to a service command to obtain initial data to assign to the observables.

The init() function must finally call bindDefaultSection() to bind the observables to the view and successFuntions() which load subsequent view models if needed.

  me.bindDefaultSection();
  if (successFunction) {
      successFunction();
  }

Often the init() function will have a series of loader function calls and maybe a service command each of which will have an annoymous function that executes on successfull completion. It might look something like this:

init(successFunction?: () => void) {
  let me = this;
  me.application.loadResources([..]),() => {
    me.application.loadComponents('..', () => {
      me.services.executeService('GetInitialData',null, 
              function(serviceResponse: Peanut.IServiceResponse) {
                if (serviceResponse.Result == Peanut.serviceResultSuccess) {
                  // assign observables

                  // and finally ...
                  me.bindDefaultSection();
                  if (successFunction) {
                    successFunction();
                  }
                }
              }
      );
    });
  };
}

Note how the 'success' functions wait for each predecessor task to complete and finally the binding and final success function takes place.

Loader functions

The Peanut Application object provides a number of functions for use in initialization that load components and resources. [1]

Each of these functions take as their first parameter a list of resource, either as an array of strings or a comma delimited list in a single string. Example:

me.application.registerComponents([
    '@pnut/month-lookup,',
    '@pnut/modal-confirm',
    '@pnut/pager',
    '@pnut/multi-select'
    ], () => {
        me.application.loadResources([
          '@lib:local/moment-js',
          '@pnut/ViewModelHelpers',
          '@lib:fullcalendar-js',
          '@lib:tinymce'
        ], () => {
            . . .
      });
    });    

Typically these resources references start with a token that begins with '@'. These are used by the Peanut loader to locate the resource:

Component Loading

For usage examples of 'registerComponent' and late binding techniques with 'loadComponent' see the test page 'Component Test' with source in: web.root/application/peanut/tests/vm/ComponentsTestViewModel.ts

See also: Components Overview