We use "component" here to mean any Typescript/HTML code that can be reused across peanut ViewModel/Views. They include:
Based on Knockout js component model. Like a viewmodel, has a view model class match with an HTML templated. Peanut has special support for dynamic loading of thes components.
Components models are stored in a 'components' and named by convention:
componentName + 'Component.ts
e.g. personsSelectorComponent.ts
Templates are store in a 'templates' directory parallel to the components directory and named by convention:
componentName + '.html'
Here's a template for the component viewmodel:
/// <reference path='../../typings/knockout/knockout.d.ts' />
namespace Peanut {
export class componentnameComponent {
// observables
// variables.
// include constructor if any params used
constructor(params: any) {
let me = this;
if (!params) {
console.error('componentnameComponent: Params not defined in translateComponent');
return;
}
// initialize observavles and variables from params
/* Example:
if (!params.parameterName) {
console.error('componentnameComponent: Parameter "parameterName" is required');
return;
}
*/
}
}
}
Component templates are an ordinary HTML fragment with data bindings to the component view model.
Components are registered or loaded in the ViewModel.init() function using the application.registerComponent method.
me.application.registerComponents('@pnut/modal-confirm', () => {
});
Sometimes the view model needs some control over the component, for instance to load or clear data. In this case late binding can be used so that the view model may create the component vm and keep the reference. In this technique, the application.loadComponent() method is used to reference the component source and templalte then registerComponent is called passign in an instance of the model.
me.application.loadComponents('tests/message-constructor', () => {
let cvm = new messageConstructorComponent('Smoke Test Buttons:');
me.application.registerComponent('tests/message-constructor', cvm, () => {
me.bindDefaultSection();
successFunction();
});
});
The component can communicate with the owning ViewModel using one or more of these techniques.
<translate params="code:'form-legend',translator:self"></translate>
<pager params='click:onPagerClick,page:currentPage,max:maxPages,waiter:refreshing,owner:self'></pager>
The handle event method is typically used for communication between objects such as knockout components. An evant handler method is defined in the interface IEventHandle (peanut.d.ts)
export interface IEventSubscriber {
handleEvent : (eventName: string, data?: any) => void;
}
ViewModelBase implements IEventSubscriber with an empty method, so overiding is optional and calls to handleEvent have not effect if not overriden in the concrete view model class.
A reference to the view model may be passed using the constructor or in the markup, and implemented in view model like this:
handleEvent = (eventName:string, data?:any)=> {
let me = this;
switch (eventName) {
case 'person-selected' :
me.newTerm(<INameValuePair>data.person);
break;
case 'person-search-cancelled' :
// alert('Search cancelled.');
break;
case 'test' :
alert('event test');
break;
}
};
selectPerson = (personItem:INameValuePair)=> {
var me = this;
me.hide();
if (me.owner) {
me.owner.handleEvent('person-selected',
{person: personItem, modalId: me.modalId()}
)
}
};
These classes encapsulate a set of observables and may be used in different view models. Implementation of the view is done in the specific ViewModel/View that hosts the instance of this class.
Often used for forms.
Class example:
/pnut/packages/qnut-directory/js/PersonObservable.ts
Usage example:
/pnut/packages/qnut-directory/vm/UserProfileViewModel.ts
Or used for special ui components. Class example:
/pnut/js/searchListObservable.ts
Usage example:
/pnut/packages/qnut-directory/js/DirectoryEntities.ts
These are simply code fragments that may be extracted from an existing ViewModel and View and adapted for use elsewhere.
They may be suitable for later refactoring to a component or reusable class.
Type: knockout component
Encapsulates select list handling for INameValue[]
In init() function
me.application.loadResources([
'@pnut/selectListObservable'
], () => {
});
me.taskEditForm.intervalType= new Peanut.selectListObservable(
// handler - null if no handler
me.onIntervalTypeChange,
// selection array
[{Name: 'On demand', Value: 1},
{Name: 'Regular interval', Value: 2},
{Name: 'Weeky', Value: 3},
{Name: 'Daily', Value: 4},
{Name: 'Fixed time', Value: 5}],
// initial default value
1 );
HTML in view example
<label for="document-types">Document type</label>
<select class="form-control form-select" id="document-types"
data-bind = "options: documentType.options, optionsText:'Name', value:documentType.selected"
></select>
for selectlist with handlers you need to call subscrible and unsubscribe
this.taskEditForm.intervalType.unsubscribe();
if (value) {
this.taskEditForm.intervalType.setValue(value);
}
else {
this.taskEditForm.intervalType.setDefaultValue();
}
this.taskEditForm.intervalType.subscribe();
If list is of type ILookupItem[] use assignLookupList:
me.sessiontypeSelector.assignLookupList(response.sessionTypes);
Type: knockout component
Supports user input of multiple associated values to an "array" field.
Source:: /knockout_view/pnut/components/multiSelectComponent.ts Template:: /knockout_view/pnut/templates/multiSelect.html Example: /application/peanut/tests/vm/TestPageViewModel.ts and /view/TestPage.html
<multi-select params="items:itemList, selected:selectedItems,sort:'code',
translator:self, label:'Multiselect', caption:'Select something...'">
</multi-select>
Data is in the form of ILookupItem[]
export interface ILookupItem {
id : any;
code: string;
name: string;
description : string;
}
Initialized from a Peanut ViewModel init() function.
// register the component
me.application.registerComponents('@pnut/multi-select', () => {
});
Lodash dependencies remove in SCYM version. 1/22/2022. Older versions require lodash library to be loaded.
me.application.loadResources(['@lib:lodash'], () => {
});
Type: Observable container class.
A class for supporting presentation of a UI component allowing a pageable selection list based on a query selection result.
Source code:
/pnut/js/searchListObservable.ts
Usage example, within a Knockout component:
/pnut/packages/qnut-directory/components/personSelectorComponent.ts
Usage within a ViewModel:
/pnut/packages/qnut-directory/vm/DirectoryViewModel.ts
me.application.loadResources([
'@pnut/ViewModelHelpers',
'@pnut/editPanel',
'@pnut/searchListObservable',
], () => {
});
me.personsList = new Peanut.searchListObservable(2, 12);
Note references to personList methods and observables. Also the use of $parent to reference the owning view model.
<div id="findPersonResults" class="row found well" data-bind="visible:personsList.selectionCount">
<div id="persons-found-header" class="row">
<div class="col-md-12">
<translate params="code:'label-found',translator:self"></translate>: <span data-bind="text: personsList.selectionCount"></span> <translate params="code:'dir-person-entity-plural',translator:self"></translate>.
</div>
</div>
<div id="persons-found-nav" class="row">
<div class="col-md-6" style="padding-right: 3px" data-bind="visible:personsList.hasPrevious">
<a href="#" data-bind="click:personsList.previousPage"><span class="fa fa-backward"></span> <translate params="code:'nav-previous',translator:self"></translate></a>
</div>
<div class="col-md-6" style="padding-right: 3px" data-bind="visible:personsList.hasMore">
<a href="#" data-bind="click:personsList.nextPage"><translate params="code:'nav-more',translator:self"></translate> <span class="fa fa-forward"></span></a>
</div>
</div> <!-- end found header -->
<div class="row" id="persons-found-list">
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6" data-bind="foreach: personsList.selectionList[1]">
<div class="link-list-item">
<a href="#" data-bind="click:$parent.addPersonToAddress"><span data-bind="text: Name"></span></a>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6" data-bind="foreach: personsList.selectionList[2]">
<div class="link-list-item">
<a href="#" data-bind="click:$parent.addPersonToAddress"><span data-bind="text: Name"></span></a>
</div>
</div>
</div> <!-- end found list -->
</div>
These methods and observables are typically used in the owning view model:
Type: Model/View Pattern
Extracted from:
/pnut/packages/qnut-directory/view/Directory.html
This example present a searchable selection list is presented in order for the user to select multiple affiliation values.
<div class="form-group">
<div>
<span style="font-weight: bold"><translate params="code:'dir-label-organization',translator:self"></translate>: </span>
</div>
<div class="input-group" style="margin-bottom: 10px">
<input class="form-control" id="orgsearch" type="search" name="search" placeholder="Select or search for an organization"
data-bind="value: personForm.orgSearchValue, valueUpdate: 'keyup'" autocomplete="off" />
<span class="input-group-btn"><button class="btn btn-default btn-outline-secondary" type="button" data-bind="click:personForm.onShowOrgList">
<i data-bind="visible:personForm.orgListVisible" class="fa fa-caret-up"></i>
<i data-bind="visible:!personForm.orgListVisible()"class="fa fa-caret-down"></i>
</button></span>
</div>
<div style="position:absolute; z-index: 2; clear: both;background-color: white; max-height: 25ex; overflow: scroll; border: 1px solid lightgrey; padding: 10px"
data-bind="visible:personForm.orgListVisible">
<table class="table table-bordered" >
<tbody data-bind="foreach:personForm.orgLookupList">
<tr>
<td data-bind="text:name"></td>
<td><a href="#" data-bind="click:$parent.personForm.onOrgSelect">
<translate params="code:'label-select',translator:$parent.self"></translate></a> </td>
</tr>
</tbody>
</table>
</div>
</div>
Note the data-bind prameters to the input control:
data-bind="value: personForm.orgSearchValue, valueUpdate: 'keyup'" autocomplete="off"
The view model code is in the observable container class personObservable:
/pnut/packages/qnut-directory/js/PersonObservable.ts
Here the orgSearchValue is subscribed to a method that filters the list, orgLookupList, referenced in the table below.
onOrgSearchChange = (value: string) => {
let me = this;
if (value) {
me.selectedOrganization(null);
me.orgLookupList([]);
value = value.toLowerCase();
let newlist = me.organizations.filter((org: Peanut.ILookupItem) => {
return (org.name.toLowerCase().indexOf(value) >= 0);
});
me.orgLookupList(newlist);
me.orgListVisible(newlist.length > 0);
}
else {
me.orgLookupList(me.organizations);
}
};
The "valueUpdate: 'keyup'" binding causes orgSearchValue() to be updated as the user types, thus triggering the onOrgSearchChange() method to refilter the list on every key stroke.
Also note the styles on the DIV element containing the table which position the selection list as a drop-down.
This component allows the a data management page for any entity that supports the ILookupItem interface.
Required:
Optional:
The view contains only the component tag and parameters.
Example:
<div id="quarterlymeeting-load-message"><span class="fa fa-spinner fa-pulse fa-2x" style="color:lightgrey"></span></div>
<div id="quarterlymeeting-view-container" class="row" style="display: none">
<lookup-edit params="
items:quarterlies,
onUpdate:updateQuarterly,
name:'quarterly',
plural:'quarterlies',
canEdit:userCanEdit">
</lookup-edit>
</div>
The View model resposibilities are: