Enyo Basics: Encapsulation

1 BY adahm

For developers that are just getting started with webOS 3.0 and familiarizing themselves with our Enyo framework, the concept of encapsulation – a way to break your app down into smaller, self-contained parts – can make your code easier to understand and maintain.

Once an Enyo app is broken down into self-contained components, they need to be able to interact with each other to perform actions or expose and change properties they contain. In this blog post we will explore encapsulation using Enyo components and the methods that can be used to interact with them.

A simple example

Here is a simple Enyo application without encapsulation. The app will be modified to encapsulate some of its functionality later. The app is a timer that will show a progress bar increasing in value from 0% to 100% over the course of 10, 30 or 60 seconds. It also has radio buttons that determine the duration of the timer and a button to start the timer.

Here is the original code for the app without any encapsulation besides that provided by the built-in Enyo controls themselves. Even with this simple example, it’s easy to see how the code can start to get out of hand as more components are added.

enyo.kind({
  name:  'SimpleTimerApp',
  kind:  'enyo.Control',
  components: [
    {name: "simpleTimer", kind: "RowGroup", caption: "Simple Timer", components: [
      {name: "animator", kind: enyo.Animator, easingFunc: enyo.easing.linear,
        duration: 30000, tick: 1000, onBegin: "beginAnimation",
        onAnimate: "stepAnimation", onEnd: "endAnimation"
      },
      {kind: "Control", layoutKind: "HFlexLayout", pack: "start", align: "center",
         components: [
           {name: 'timerProgress', kind: "ProgressBar", flex:1, minimum: 0,
             maximum: 30, position: 0}
      ]}
    ]},
    {kind: "Control", layoutKind: "HFlexLayout", pack: "center", align: "center",
      components: [
        {name: "selTimerDuration", kind: "RadioGroup", width: "360px",
           onChange: "timerDurationChanged", components: [
            {caption: "10 Sec.", value: "10"},
            {caption: "30 Sec.", value: "30"},
            {caption: "60 Sec.", value: "60"}
      ]}
    ]},
    {kind: "Control", layoutKind: "HFlexLayout", pack: "center", align: "center",
       components: [
        {name: 'startTimer', kind:'Button', caption:'Start Timer', width:'360px',
          onclick:'timerStart' }
    ]}
  ],

  constructor: function () {
    this.inherited(arguments);
    this.timerDuration = 30;
  },

  create: function () {
    this.inherited(arguments);

    this.$.selTimerDuration.setValue(this.timerDuration);

    this.$.simpleTimer.setCaption("Simple Timer (" + this.timerDuration + " seconds)");
    this.$.timerProgress.setMaximum(this.timerDuration);
    this.$.animator.setDuration(this.timerDuration * 1000);
  },

  timerStart: function () {
    this.$.animator.play(0, this.timerDuration);
  },

  stepAnimation: function(inSender, inValue) {
    this.$.timerProgress.setPosition(inValue);
  },

  beginAnimation: function(inSender, inStart, inEnd) {
    this.$.timerProgress.setPosition(0);
    this.$.startTimer.setDisabled(true);
  },

  endAnimation: function(inSender, inValue) {
    this.$.timerProgress.setPosition(0);
    this.$.startTimer.setDisabled(false);
  },

  timerDurationChanged: function(inSender, inValue, inOldValue) {
    this.timerDuration = inValue;
    this.$.simpleTimer.setCaption("Simple Timer (" + inValue + " seconds)");
    this.$.timerProgress.setMaximum(inValue);
    this.$.animator.setDuration(inValue * 1000);
  }
});

Creating a control

A general programming rule of thumb is that indentation should not go more than three levels deep. Examination of the components section of the source code shows that the app can be broken down into three parts. One is the timer itself, which consists of a progress bar to provide feedback, an animator control and a few other controls. The second part contains the radio buttons for setting the duration of the timer, and the third part is the button to start the timer. All the components in the first part make up the actual timer used in the app and are related to each other, so that will become a custom control named SimpleTimer that will be broken out into its own Enyo kind at the same level as the app kind.

enyo.kind({
  name:  'SimpleTimer',
  kind: "RowGroup",
  caption: "Simple Timer",
  published: {
    timerDuration: 30
  },
  events: {
    onSimpleTimerStart: "",
    onSimpleTimerEnd: ""
  },
  components: [
    {name: "animator", kind: enyo.Animator, easingFunc: enyo.easing.linear,
      duration: 30000, tick: 1000, onBegin: "beginAnimation",
     onAnimate: "stepAnimation", onEnd: "endAnimation"
    },
    {kind: "Control", layoutKind: "HFlexLayout", pack: "start", align: "center",
       components: [
        {name: 'timerProgress', kind: "ProgressBar", flex:1, minimum: 0,
           maximum: 30, position: 0
         }
    ]}
  ],

  create: function () {
    this.inherited(arguments);
    this.setCaption("Simple Timer (" + this.timerDuration + " seconds)");
    this.$.timerProgress.setMaximum(this.timerDuration);
    this.$.animator.setDuration(this.timerDuration * 1000);
  },

  start: function () {
    this.$.animator.play(0, this.timerDuration);
  },

  stepAnimation: function(inSender, inValue) {
    this.$.timerProgress.setPosition(inValue);
  },

  beginAnimation: function(inSender, inStart, inEnd) {
    this.$.timerProgress.setPosition(0);
    this.doSimpleTimerStart();
  },

  endAnimation: function(inSender, inValue) {
    this.$.timerProgress.setPosition(0);
    this.doSimpleTimerEnd();
  },

  timerDurationChanged: function() {
    this.setCaption("Simple Timer (" + this.timerDuration + " seconds)");
    this.$.timerProgress.setMaximum(this.timerDuration);
    this.$.animator.setDuration(this.timerDuration * 1000);
  }
});

enyo.kind({
  name:  'SimpleTimerApp',
  kind:  'enyo.Control',
  components: [
    {name: "simpleTimer", kind: "SimpleTimer",
       onSimpleTimerStart: 'simpleTimerStarted',
       onSimpleTimerEnd: 'simpleTimerEnded'
    },
    {kind: "Control", layoutKind: "HFlexLayout", pack: "center", align: "center",
        components: [
        {name: "timeLimit", kind: "RadioGroup", width: "360px",
           onChange: "radioButtonSelected", components: [
             {caption: "10 Sec.", value: "10"},
             {caption: "30 Sec.", value: "30"},
             {caption: "60 Sec.", value: "60"}
      ]}
    ]},
    {kind: "Control", layoutKind: "HFlexLayout", pack: "center", align: "center",
      components: [
        {name: 'startTimer', kind:'Button', caption:'Start Timer',
           width: '360px', onclick:'timerStart'
        }
    ]}
  ],

  create: function () {
    var initialDuration = 10;
    this.inherited(arguments);
    this.$.timeLimit.setValue(initialDuration);
    this.$.simpleTimer.setTimerDuration(initialDuration);
  },

  radioButtonSelected: function (inSender) {
    this.$.simpleTimer.setTimerDuration(inSender.getValue());
  },

  timerStart: function () {
    this.$.simpleTimer.start();
  },

  simpleTimerStarted: function () {
    this.$.startTimer.setDisabled(true);
  },

  simpleTimerEnded: function () {
    this.$.startTimer.setDisabled(false);
  }

});

Communicating with the control

In the original sample application, code that was used by the timer was spread out throughout the application. Changing any part of the timer code meant the possibility of affecting other code in the application. But now that the code for the new SimpleTimer control is encapsulated in its own kind, how does the app communicate with the components within the control? The SimpleTimer control acts as a gateway or buffer between the application and whatever components and code are inside the control.

Calling methods in the control

To tell the control to do something, methods belonging to the control are called. The SimpleTimer control has a start() method that starts the timer. All the work that goes into initializing the internal controls and starting the timer happens inside the SimpleTimer control itself.

Receiving events

When the control needs to tell the app about something, it sends events. The SimpleTimer sends simpleTimerStart and simpleTimerEnd events to the application. In this particular implementation, the events start out as onBegin and onEnd events from the Animator control. The SimpleTimer control processes these internal events and passes them on as its own simpleTimerStart and simpleTimerEnd events to the application.

Getting and setting properties

Properties within the control can be published to give the application access to them. When a property is published, it automatically gets a get<property name>() and a set<property name>() method. If a <property name>Changed() method is created, it will get called whenever there is a change to the property. In the SimpleTimer control, the timerDuration property was published, so it has the getTimerDuration() and setTimerDuration() methods. A timerDurationChanged() method was also created to update the animator control and some UI controls whenever the timerDuration property is changed.

Conclusion

The overall size of the app has increased slightly, but the code is easier to maintain because everything related to the SimpleTimer component is contained within the kind. The component can be used in several places within the app and can be easily copied and used in other projects. Thanks to encapsulation, the inner workings of the component can be changed with little to no impact on the app itself – for instance, swapping out the animator control with some other sort of timer code or changing the UI from a progress bar to a numeric display.

 

Comment (1)

  1. gollyzila says:

    Thanks for this helpful article. The section about using events for the control to communicate with the app made encapsulation more understandable.