SharePoint Framework & KnockoutJS – Simplifying Properties with an Event Bus

In my last post I took a good look at the way custom property values are passed around in the knockout template of an SPFX web part project. I found it really confusing and it took me a little while to understand what was going on – so I thought it might be useful to record my findings: https://codeforcloud.com/2016/10/13/sharepoint-framework-with-knockoutjs-making-sense-of-custom-properties/ .

In this post we’re looking at how we can simplify the management of property values using an EventBus. This is a pretty common method in a modular javascript project – it’s a common module / class that we reference from a number of places. Then we use knockout observables and subscriptions to publish / subscribe to values. It helps to decouple the classes we get in the default project.

In short, we’re going to:

  • Create a new EventBus class that holds all our properties we want to pass around
  • Push our property changes into the eventbus
  • Subscribe / read property updates from the eventbus in our ViewModels / views.

It’s simpler to see it in action.

  1. Create the EventBus class. Here we use the singleton pattern to create a new class – this is important as we only ever want one event bus running. Add all your own properties in here (you’ll see “myProp” I’m using for demo)

    import * as ko from 'knockout';
    export class EventBus {
    private static _instance:EventBus = new EventBus();
    // props for pub-sub
    public myProp:KnockoutObservable<string> = ko.observable('');
    constructor(){
    if(EventBus._instance){
    throw new Error('Cannot instantiate. Use EventBus.getInstance() instead');
    }
    EventBus._instance = this;
    }
    public static getInstance():EventBus{
    return EventBus._instance;
    }
    }
    view raw EventBus.ts hosted with ❤ by GitHub
  2. Import your EventBus into your WebPart class, and push values into it. Have a look through the code and we’ll point out the important lines below:
    import * as ko from 'knockout';
    // ...other imports omitted...
    import { EventBus } from './EventBus';
    let _instance: number = 0;
    export default class CoffeeRoundWebPart extends BaseClientSideWebPart<any> {
    private _id: number;
    private eventBus = EventBus.getInstance();
    public constructor(context: IWebPartContext) {
    super(context);
    this._id = _instance++;
    }
    private initWorkaround():void{
    const tagName: string = `CoffeeRoundComponent-${this._id}`;
    const componentElement: HTMLElement = this._createComponentElement(tagName);
    this._registerComponent(tagName);
    this.domElement.appendChild(componentElement);
    var vm = new CoffeeRoundViewModel();
    ko.applyBindings(vm, this.domElement);
    }
    public render(): void {
    console.log('render');
    if (!this.renderedOnce)
    {
    this.initWorkaround();
    }
    // push vals into eventbus with new values
    this.eventBus.myProp(this.properties.myProp || '');
    }
    }

    Take note of:

    1. Line 10:  private eventBus = EventBus.getInstance(); . This gets the instance of the event bus we want – remember we’re using a singleton.
    2. Line 36: this.eventBus.myProp(this.properties.myProp || ”); . The render() method is called when properties in the web part are changed by the user – so here we push the new values (given to us in this.properties) into the event bus.
  3. Import your EventBus into your ViewModel / other modules, and use the values. Again have a look at my ViewModel code and we’ll pull out the highlights below:
    import * as ko from 'knockout';
    // ...imports omitted...
    import {EventBus} from './EventBus';
    export default class CoffeeRoundViewModel {
    // ...other vars omitted...
    private eventBus = EventBus.getInstance();
    constructor() {
    var self = this;
    self.eventBus.myProp.subscribe(
    function(){
    // this is called whenever the value changes
    console.log('Subscribe: ' + self.eventBus.myProp());
    });
    }
    }

    Take note of:

    1. Line 9: private eventBus = EventBus.getInstance(); . Again, we get the instance of the EventBus we’re using elsewhere.
    2. Line 14: self.eventBus.myProp.subscribe… Using a knockout subscription we can – optionally – subscribe to changes in the value. This means we don’t actually need to recreate the property variables in our ViewModel (or the webpart class, for that matter). 
  4. Reference the properties in a View. To complete the circle, we can directly reference our property values in our view markup, should you wish. As stated above, this means we don’t need to have multiple “var myProp” declarations in our project – we only declare the property values in the EventBus, and just reference them elsewhere.

    <div class="container">
    <h1 data-bind="text: $parent.eventBus.myProp()"></h1>
    </div>

 

Tidying Up…

When we use the above approach, it means we don’t actually need the following (I’d like to get some word from MS on the below really…)

  • IMyWebPartProps.ts interface (so instead your web part declaration might look like: export default class MyCoolWebPart extends BaseClientSideWebPart<any>…)
  • The private _myProp variables in the web part class
  • The _shouter object in the web part class
  • Any subscriptions / .notifySubscribers calls in the web part class
  • The IBindingContext class in your ViewModel, and the instantiation of it in your WebPart. That can all go. Instead, you can just new up your ViewModel in the WebPart class and bind with that instead:

    var vm = new MyCoolViewModel();
    ko.applyBindings(vm, this.domElement);

Conclusion

The above approach has a number of benefits, in my view:

  • Separation of concerns – the classes are much less tightly coupled
  • Less plumbing code
  • Less subscriptions and property values bouncing around
  • Simpler debugging when things go wrong.

Any comments or suggestions on the above, i’d love to hear them. Otherwise – I hope it’s useful 🙂

 

 

 

Leave a comment

About davros85

Software Engineer @ Microsoft, working with key customers to help them be successful on Azure