Posts

  • Enyo Daily #9 - Stateful

    I came across this interesting kind while investigating the Picker control.  It's pretty basic -- 1 property and 1 method (+1 protected) -- but can be a useful base class to streamline the styling of a control.

    [[MORE]]

    Stateful's single method is setState() which expects attribute as a string and its state as a boolean.  This method in turn adds or removes a css class with the attribute name as the suffix.  The prefix for the class name is set using the single published property of Stateful -- cssNamespace.  The default cssNamespace is "enyo" but I'd recommend changing it to something unique to your application.

    While you could create a Stateful kind directly, it's designed to be used as a base class.  For example, CustomButton (which is one of the base kinds for the Picker control), publishes several additional properties; each of the changed handlers for these properties call Stateful.setState to style the control appropriate for the change in property value.

    An example will help illustrate.  For a further example, take a look at the source for CustomButton.

    CSS

    .extras-active {
      background-color:red;
    }

    JavaScript

    enyo.kind({
      name:"StatefulButton",
      kind:"Stateful",
      cssNamespace:"extras",
      published:{
        active:false
      },
      activeChanged:function() {
        this.setState("active", this.active);
      },
      clickHandler:function() {
          this.setActive(!this.active);
      }
    });

    enyo.kind({
      name:"com.technisode.example.App",
      kind:"VFlexBox",
      components:[
        {kind:"StatefulButton", active:true, content:"Button 1"},
        {kind:"StatefulButton", active:false, content:"Button 2"},
        {kind:"StatefulButton", active:false, content:"Button 3"}
      ]
    });

  • Enyo Daily #8 - Events - Part 3

    In the exciting finale on events in enyo, I'll cover the ApplicationEvents component.  This built-in component handles the three window events (onload, onunload, and resize) as well as a host of webOS specific events.

    For more background on events, check out Part 1 on custom events and Part 2 on DOM events.

    [[MORE]]The ApplicationEvents component is really just a convenience control.  It contains virtually no logic.  Instead it simply overrides dispatchDomEvent (see Part 2 for details on this method) and fires its custom events instead.  Not to say there isn't value using it; it's preferable to declaratively bind a handler via a component versus programmatically checking the event type in dispatchDomEvent.  It also  (hopefully) insulates the developer for changes to the underlying event model.  For example, if HP decided to change an event type, ApplicationEvents would be able to handle that scenario and fire its original event.

    Because there isn't anything to ApplicationEvents, I won't spend much time discussing its features.  One notable event is onWindowRotated.

    As you would expect, this is fired when you rotate the device (or emulator via keystrokes).  Under the covers, this event is really just a translation of the window resize event (Search for enyo.sendOrientationChange in the enyo source to see what's going on).  You'd think that you'd be able to simulate rotation in the browser by reszing the window.  Unfortunately, there's check for changes to PalmSystem.screenOrientation (which I assume is set by Palm's customized WebKit since it isn't referenced elsewhere in source) so windowRotated never fires in the browser.

    If you want to test how your display will scale when changing from portrait to landscape, you can add a resizeHandler method to your component (check out Part 2 for an explanation why you don't have to specifically listen for resize for resizeHandler to be called).  If, however, you need to test for a specific orientation, you'll have to use the emulator.

    Here's the complete example incorporating all three types of events.

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"ApplicationEvents", name:"appEvents"},
            {kind:"EventSender", name:"eventSender", onSend:"sent"},
            {kind:"Button", onclick:"clicked"},
            {kind:"Input", onkeypress:"press"}
        ],
        create:function() {
            this.inherited(arguments);
           
            // map all ApplicationEvents to logAppEvent
            var ae = this.$.appEvents;
            Object.keys(enyo.ApplicationEvents.prototype.events).forEach(function(key) {
                enyo.log("setting handler for ",key);
                ae[key] = "logAppEvent";
            })
        },
        logAppEvent:function(source, e) {
            enyo.log("logAppEvent",e.type);
        },
        resizeHandler:function() {
            enyo.log("dimensions",window.document.body.offsetWidth,window.document.body.offsetHeight);
        },
        captureDomEvent:function(e) {
            enyo.log("A DOM event occured", e.type);
           
            // returning true would indicated the event is captured and prevent the bubble phase
            // thereby preventing the declared handlers (clicked in this case) from being called
           
            // returning false (or no explicit return) lets things continue
            return false;
        },
        dispatchDomEvent:function(e) {
            // like any other method, you could override dispatchDomEvent and implement custom routing
            e.myCustomField = "This is a custom field";
           
            return this.inherited(arguments);
        },
        press:function(source, event) {
            enyo.log(source, event);
        },
        clickHandler:function(source, event) {
            enyo.log("bubbled up to me", event.myCustomField);
           
            // calling event.stopPropagation() or returning true will end the bubble phase
        },
        clicked:function(source, event) {
            // trigger my custom events
            this.$.eventSender.go();
           
            // toggles event handler between send and secondSent ... just because ...
            this.$.eventSender.onSend = (this.$.eventSender.onSend === "sent") ? "secondSent" : "sent";
           
            // calling event.stopPropagation() or returning true will end the bubble phase
        },
        sent:function(source, one, two, three) {
            enyo.log("sent", one, two, three)
        },
        handleOnAlert:function(source, obj) {
            enyo.log("alerted", enyo.json.stringify(obj));
        },
        secondSent:function(source, one, two, three) {
            enyo.log("secondSent handles onSend now", one, two, three)
        }
    }

    var _EventSender = {
        name:"EventSender",
        kind:"Component",
        events:{
            onSend:"handleOnSend",
            onAlert:{value:"handleOnAlert", caller:"sendAlert"}
        },
        go:function() {
            this.doSend(1,2,3);    // dispatchIndirectly
            this.sendAlert({a:1, b:2});
        }
    }

    enyo.kind(_EventSender);
    enyo.kind(_Example);

  • Enyo Daily #7 - Events - Part 2

    In Part 1 about Events in enyo, I covered defining, triggering, and reacting to custom events for enyo Components.  The other source is DOM events triggered by user interactions or window changes.[[MORE]]

    First off, here are the supported DOM events in enyo:

    Document Events
    mousedown, mouseup, mouseover, mouseout, mousemove, click, dblclick, change, keydown, keyup, keypress, input

    Window Events
    resize, load, unload

    There are two phases to DOM event handling in enyo:  capture and bubble.  The capture phase starts with the eldest ancestor, calls captureDomEvent (if found), and continues downt the hierarchy until a captureDomEvent call returns true or reaches the source Control of the event.

    If nothing captures the event, it's passed to the bubble phase.  This phase traverses the opposite direction -- from node to parent.  Again, if an event handler (dispatchDomEvent in the bubble phase) returns true, the phase stops; otherwise it continues until it reaches the top of the DOM tree.  Also, if stopPropagation() is called on the event object, the phase will end as well.

    Unlike the capture phase where you must implement captureDomEvent to handle the event, enyo.Component implements dispatchDomEvent and provides some basic routing.  It first looks for a method matching the signature eventtypeHandler (e.g. clickHandler).  If not found, it will look for an oneventtype (e.g. onclick) declaration and dispatch the event to it.  Note that these handlers are mutually exclusive; if the first is found, the second will not be called.

    Below is a slightly expanded example from the previous part that includes DOM event capturing and bubbling.  If you try this code out, you can add different return values to see when events are stopped in each phase.

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"EventSender", name:"eventSender", onSend:"sent"},
            {kind:"Button", onclick:"clicked"},
            {kind:"Input", onkeypress:"press"}
        ],
        captureDomEvent:function(e) {
            enyo.log("A DOM event occured", e.type);
           
            // returning true would indicated the event is captured and prevent the bubble phase
            // thereby preventing the declared handlers (clicked in this case) from being called
           
            // returning false (or no explicit return) lets things continue
            return false;
        },
        dispatchDomEvent:function(e) {
            // like any other method, you could override dispatchDomEvent and implement custom routing
            e.myCustomField = "This is a custom field";
           
            return this.inherited(arguments);
        },
        press:function(source, event) {
            enyo.log(source, event);
        },
        clickHandler:function(source, event) {
            enyo.log("bubbled up to me", event.myCustomField);
           
            // calling event.stopPropagation() or returning true will end the bubble phase
        },
        clicked:function(source, event) {
            // trigger my custom events
            this.$.eventSender.go();
           
            // toggles event handler between send and secondSent ... just because ...
            this.$.eventSender.onSend = (this.$.eventSender.onSend === "sent") ? "secondSent" : "sent";
           
            // calling event.stopPropagation() or returning true will end the bubble phase
        },
        sent:function(source, one, two, three) {
            enyo.log("sent", one, two, three)
        },
        handleOnAlert:function(source, obj) {
            enyo.log("alerted", enyo.json.stringify(obj));
        },
        secondSent:function(source, one, two, three) {
            enyo.log("secondSent handles onSend now", one, two, three)
        }
    }

    var _EventSender = {
        name:"EventSender",
        kind:"Component",
        events:{
            onSend:"handleOnSend",
            onAlert:{value:"handleOnAlert", caller:"sendAlert"}
        },
        go:function() {
            this.doSend(1,2,3);    // dispatchIndirectly
            this.sendAlert({a:1, b:2});
        }
    }

    enyo.kind(_EventSender);
    enyo.kind(_Example);

  • Enyo Daily #6 - Events - Part 1

    I should probably talk about the design goals addressed by events but that's not a topic I'm up for tonight.  Instead, I'll jump into how events are handled in enyo and defer design talk for another day.[[MORE]]

    There are two kinds of events in enyo:  DOM and custom.  DOM events are fired as a result of user interaction such as clicking a control, changing a value in a field, or resizing the window.  Custom events are those defined on an enyo kind that are triggered at the discretion of the component.

    Custom events are simpler so we'll start with those.  These events are declared in the kind definition inside the "events" property.  Enyo expects events to be prefixed by "on" and will both warn you and add the prefix itself if it discovers an event without it.

    {
      name:"ComponentName",
      events:{
        onEventName:""
      }
    }

    During the component creation process, enyo will automatically add a function to the component which will "fire" the event.  By default, the function will match the event name with "on" replaced with "do" (doEventName in the above example).  This is actually customizable by specifying an object instead of an empty string as the value following the event name.  For example, if you prefered fireEventName instead of doEventName, use this syntax:

    {
      name:"ComponentName",
      events:{
        onEventName:{caller:"fireEventName"}
      }
    }

    You might be wondering why require an object literal to specify the caller if it's just a string; why not just pass "fireEventName" as the value and skip the object.  The reason is that you can also specify a default handler name for the method.  If the value following the event is a string, it is treated as the default handler name.  If it's an object, it must be included as the value of the "value" property of that object.

    {
      name:"ComponentName",
      events:{
        onEventName:{caller:"fireEventName", value:"handleEventName"},
        onAnotherEvent:"handleAnotherEvent"
      }
    }

    If you're using a component that publishes an event, you can react to it by declaring the name of the handler method in the component declaration.

    components:[
      {kind:"ComponentName", onEventName:"myEventHandler"}
    ],
    myEventHandler:function(source) {
      // do something
    }

    If the kind specified a default handler (as illustrated above) and the owning object has a method of the same name, the declaration can be omitted.

    components:[
      {kind:"ComponentName", onEventName:"myEventHandler"}
    ],
    myEventHandler:function(source) {
      // do something
    },
    handleAnotherEvent:function(source) {
      // will be found automatically since it matches the default handler name
    }

    Finally, here's a complete example.  I've included some DOM event stuff that I'll cover in a future topic.

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"EventSender", name:"eventSender", onSend:"sent"},
            {kind:"Button", onclick:"clicked"}
        ],
        captureDomEvent:function(e) {
            enyo.log("A DOM event occured", e.type);
           
            // returning true would indicated the event is captured and prevent the bubble phase
            // thereby preventing the declared handlers (clicked in this case) from being called
           
            // returning false (or no explicit return) lets things continue
            return false;
        },
        clicked:function() {
            // trigger my custom events
            this.$.eventSender.go();
           
            // toggles event handler between send and secondSent ... just because ...
            this.$.eventSender.onSend = (this.$.eventSender.onSend === "sent") ? "secondSent" : "sent";
        },
        sent:function(source, one, two, three) {
            enyo.log("sent", one, two, three)
        },
        handleOnAlert:function(source, obj) {
            enyo.log("alerted", enyo.json.stringify(obj));
        },
        secondSent:function(source, one, two, three) {
            enyo.log("secondSent handles onSend now", one, two, three)
        }
    }

    var _EventSender = {
        name:"EventSender",
        kind:"Component",
        events:{
            onSend:"handleOnSend",
            onAlert:{value:"handleOnAlert", caller:"sendAlert"}
        },
        go:function() {
            this.doSend(1,2,3);    // dispatchIndirectly
            this.sendAlert({a:1, b:2});
        }
    }

    enyo.kind(_EventSender);
    enyo.kind(_Example);

  • Enyo Daily #5 - Lists, Repeaters, and Flyweights - Part 2

    Part 1 of this topic covered Flyweights and how they work.  Flyweights themselves are a pretty low level control and not an ideal control with which to develop.  There are a couple other controls one step up the abstraction hierarchy for rendering "lists" of items:  VirtualRepeater and Repeater.[[MORE]]

    In my current project, I needed just such a control.  Specifically, here's the problem I needed to solve:

    Render a configurable number of instances of a control laid out in a horizontal row.

    I tried 3 different approaches:

    1. Programmatic loop using createComponents()
    2. enyo.VirtualRepeater
    3. enyo.Repeater

    The first iteration used a simple for loop to create all the components in create() using createComponents().  That was functional but the resulting code was a little tough to manage (mainly due to some unique requirements for some instances of the loop).

    Next, I tried using VirtualRepeater.  This was a little better because I only had 1 set of Controls with which to interact but also introduced a new issue:  rendering the items horizontally rather than vertically.  There are actually 2 issues here.  First, VirtualRepeater doesn't appear to support layoutKind.  Second, the components defined as children of the VirtualRepeater are not, in fact, childNodes in the DOM.  Instead, there's an intermediate <div> that groups elements -- the number of which is determined by the value of stripSize.  I was able to work around this using CSS:

    .extras-hflexrepeater, .extras-hflexrepeater > * {
        display:-webkit-box;
        -webkit-box-orientation:horizontal;
        -webkit-box-pack:start;
        -webkit-box-align:stretch;
    }

    The final solution -- and the one I elected to use -- is the Repeater.  The key design difference between the VirtualRepeater and the Repeater is that whereas the VirtualRepeater uses the Flyweight to manage the DOM nodes, the Repeater simply adds more enyo Controls which each manage their own DOM nodes.  In this way, the Repeater is more akin to the programmatic loop.  In fact, that is precisely how it is implemented under the covers.

    The choice between the two repeater controls is more about preference in my opinion.  From a development perspective, you have to manage indices in one way or another -- either for the "row" or for the component id.  You gain some efficiency through the auto-selection of rows on event with the Flyweight but that can be mitigated equally well using custom properties on Controls when using the Repeater.

    I like to wrap these posts up with a functional code example.  In this case, I've included both a VirtualRepeater and Repeater with the same results to illustrate how they're both used.  Note that the VirtualRepeater code requires the CSS from above.

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"Scroller", height:"80px", vertical:false, components:[
                {kind:"VirtualRepeater", name:"fw", className:"extras-hflexrepeater", onSetupRow:"setupVirtualRepeaterRow", components:[
                    {width:"120px", components:[
                        {kind:"Control", name:"text"},
                        {kind:"Button", name:"button", onclick:"buttonClicked"}
                    ]}
                ]}
            ]},
            {kind:"Scroller", height:"80px", vertical:false, components:[
                 {kind:"Repeater", name:"repeater", layoutKind:"HFlexLayout", onSetupRow:"setupRepeaterRow"}
             ]}
        ],
        buttonClicked:function() {
            // the node is automatically mapped to the right instance on event
            // but the enyo-managed data (e.g. this.$.text.getContent()) isn't.
            // see enyo.StateManger for a mechanism to save/restore component state
            console.log("clicked",this.$.text.node.innerText);
        },
        setupVirtualRepeaterRow:function(source, index) {
            if(index<25) {
                this.$.text.setContent("I'm instance "+index);
                this.$.button.setCaption("Button #"+index);
               
                return true;
            }
        },
        setupRepeaterRow:function(source, index) {
            if(index<25) {
                // note that names have to be unique because, unlike VirtualRepeater,
                // each of these enyo.Controls will exist as children of Repeater
                return {width:"120px", components:[
                    {kind:"Control", content:"I'm instance "+index, name:"text"+index},
                    {kind:"Button", name:"button"+index, caption:"Button #"+index, onclick:"buttonClicked"}
                ]}
            }
        }
    };

    enyo.kind(_Example);

  • Enyo Daily #4 - Box Model

    The flexible box controls and layouts in Enyo are really just convenience wrappers around the standard CSS3 flexible box model.  If you haven't looked into the CSS method before, I suggest you take a few moments to look into it.  Ajaxian has a post that gives you a quick intro; Html5 Rocks has another good one.  W3C also has the draft spec for perusal; there's also an earlier draft that appears to match WebKit's implementation a little more closely.

    [[MORE]]

    There are two ways in Enyo to implement a flexible layout:  FlexBox and FlexLayout.  You could also code the CSS yourself which is completely legal so I suppose there are 3 ways ... but let's pretend you don't want to do that right now.  The key difference between the Box and the Layout is that the former is a Control and the latter a Component.  This means that HFlexBox and VFlexBox are renderable DOM nodes whereas HFlexLayout and VFlexLayout are not.

    In reality, all of the relevant code is provided by the Layout components.  The HFlexBox and VFlexBox are simply Controls with layoutKind set to their appropriate layout.  I've found that I use the FlexBoxes when I just need a generic control to layout child controls and the FlexLayouts when I'm using a more complex kind.

    One key difference between the "usual" implementation of the flexible box model and enyo's is how it handles the flexed components.  Normally, a node that is flexed will be given its natural space and its portion of the remaining.  In enyo, it only receives the left over space.  It accomplishes this by setting the width of the node to 0px as an inline style.  Here's the comment from FlexLayout:

    // we redefine flex to mean 'be exactly the left over space'
    // as opposed to 'natural size plus the left over space'

    The result of this is that some components might be forced to overlap if the remaining space isn't sufficient to hold them.  Here's a quick example:

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"HFlexBox", components:[
                {flex:1, components:[
                    // on a 1024x768 screen, this button will be overlapped
                    // by the next one because it only gets the
                    // remaining space
                    {kind:"Button", width:"500px"}
                ]},
                {kind:"Button", width:"300px"},
                {kind:"Button", width:"300px"}
            ]}
        ]
    };

    enyo.kind(_Example);

  • Enyo Daily #3 - Ownership

    Ownership in Enyo rivals function binding in Mojo for the topic most likely to trip someone up when they start development.  The developer guide has a really good overview on ownership and containment but I wanted to comment on a couple additional items.[[MORE]]

    From a practical perspective, ownership affects 2 things.  First, every control has a component hash (this.$) that contains references to all other components it owns.  The owned components can be referenced by their names on the hash.  Second, when Enyo auto-wires events for a control, it looks for the declared method on the owning object.

    There are 2 primary ways in which a component ownership can be established.  The most common approach is declaratively in the components array in a kind definition.  Any component declared in this way is owned by the top level component.  The second approach is programattically via the createComponent or createComponents methods.  These methods are identical except the earlier expects a single object whereas the latter expects an array as its first parameter.  With these methods, if you do not explicitly declare the owner (via the owner member of the passed object), it will default to the caller.

    enyo.kind({
      name:"myControl",
      components:[
        {kind:"Button", name:"myButton", onclick:"clicked"},
        {kind:"Control", name:"myText", content:"Some text content"}
      ],
      create:function() {
        this.inherited(arguments);

        this.createComponent({kind:"Button", name:"newButton", "clicked"});

        // this onclick handler will not execute
        this.$.myText.createComponent({kind:"Button", name:"anotherNewButton", onclick:"clicked"});

        this.$.myText.createComponent({kind:"Button", name:"finalNewButton", owner:this, onclick:"clicked"});
      },
      clicked:function() { /* do something */}
    })

    In the above example:

    • Both myButton and myText are owned by myControl using the declarative method
    • newButton is owned by myControl by default (no owner specified and creatComponent call on myControl)
    • anotherNewButton is owned by myText by default
    • finalNewButton is owned by myControl (because owner:this is specified)

    All the onclick handlers would be mapped to myControl.clicked() except anotherButton because it is owned by myText.

    As a general rule, any components you create should be owned by the top level kind.  In other words, always pass owner:this as part of object to createComponent.  If you find yourself with a reason that a child of your kind should be the owner of the component, you probably would be better off encapsulating the child and the new component into a completely separate kind.  More on that in another post.

    A final note on containment:  Containment is primarily concerned with DOM rendering.  If you are working with (big C) Components (meaning they directly inherit from Component, not Control or its sub-kinds), containment doesn't appear to matter.  For Controls, containment directly affects the DOM tree.  So, when you're working on your app's CSS, you'll be able to reference the containment hierarchy to define your selectors.

  • Enyo Daily #2 - Lists, Repeaters, and Flyweights - Part 1

    The new approach for list components in enyo has proven to be a difficult topic to fully understand.  I don’t pretend to have a complete grasp but rummaging through the source code has helped me immensely.  Today, I want to cover the magic that is the Flyweight.

    [[MORE]]

    The Flyweight is appropriately described by the enyo API docs as “a control designed to be rendered multiple times.”  It stands to reason why this kind of a control would be good for lists.  The Flyweight enables you to define a template for a list item and the list renders the Flyweight multiple times – once for each list item.  If you’ve used the VirtualList or VirtualRepeater controls, you’ll know that you don’t have to define a Flyweight to get things running; those controls hide the Flyweight.  Nonetheless, it’s handy to understand how the Flyweight works because its design directly affects the design of the higher level controls.

    The most important feature of Flyweights is that unlike most enyo controls where the control has a single top-level node, it may have multiple nodes for a single enyo control.  This is where things begin to get murky for devs.  Consider the following code using the VirtualRepeater.  VirtualRepeater, as the name inplies, will create a node containing the controls declared in its component block for each successful return from onSetupRow.  It accomplishes this by employing the Flyweight.

    So, if in setupRow() I configure the repeater to have 10 rows, how would I later modify the text of the fifth row, for example?

    {
            kind:”VirtualRepeater”,
            name:”myRepeater”,
            components:[
                    {name:”text”},
                    {name:”button”,kind:”Button”, onclick:”buttonClicked”}
            ],
            onSetupRow:”setupRow”
    }

    As always, there are several ways to solve the problem but the one I’ll use relates to setting the “focus” of the Flyweight.  Because the Flyweight has a single component that represents multiple instances in the DOM, you have to tell it on which instance you wish to operate.  VirtualRepeater exposes a method to do such a thing:  controlsToRow(inRowIndex).  Under the covers, this method calls setNode() on the Flyweight to “focus” it on the right instance.  After that, you can reference the components as you would any other in enyo.

    highlightRow:function(rowIndex) {
            this.$.myRepeater.controlsToRow(rowIndex);
            this.$.text.setClassName(“highlighted-row”);  // invented class for illustration
    }

    The Flyweight has a nice feature that will focus on an instance in response to an event that occurs on control of that instance. Using our example above, when buttonClicked is called, the instance containing the button will automatically have focus so no additional calls are necessary:

    buttonClicked:function(source, event) {
            // no need to call controlsToRow beforehand
            this.$.text.setContent(“I’ve been clicked!”);
    }

    There is a little extra going on in VirtualList and VirtualRepeater to make this work correctly.  While Flyweight will automatically link itself to an instance's node on event and you can explicitly focus an instance using setNode(), this only ties the Flyweight to the node, not the enyo Component.  The List and Repeater components employ a StateManager component to save and restore the state of components when the Flyweight's focus changes.  Take a look in the source of those components as well as the RowServer for detail

    To wrap up, here’s a complete example using Flyweight directly to illustrate:

     var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"Flyweight", name:"fw", layoutKind:"HFlexLayout", components:[
                {kind:"Control", name:"text"},
                {kind:"Button", name:"button", onclick:"buttonClicked"}
            ]}
        ],
        buttonClicked:function() {
            // the node is automatically mapped to the right instance on event
            // but the enyo-managed data (e.g. this.$.text.getContent()) isn't.
            // see enyo.StateManger for a mechanism to save/restore component state
            console.log("clicked",this.$.text.node.innerText);
        },
        getInnerHtml:function() {
            var h = [];
           
            // make a bunch of instances of the flyweight
            for(var i=0;i<10;i++) {
                this.$.text.setContent("I'm instance "+i);
                this.$.button.setCaption("Button #"+i)
                h.push(this.getChildContent());
            }
           
            // see comment in enyo.RowServer.generateRow()
            this.$.fw.needsNode = true;
           
            return h.join("");
        }
    };

    enyo.kind(_Example);

  • Enyo Daily #1 - Preferences

    I'm planning to try a new thing here.  I'm relatively active in the Palm Dev Forums and have pulled together a list of topics that seem to be relatively common.  So, I'm hoping to do a daily (maybe, perhaps every few days ...) post on how to solve those common issues.[[MORE]]

    To start, I'm going to cover how to store preferences using the system service.  The enyo tutorial covers this briefly but unfortunately inaccurately right now.

    The low level approach is to call the system service directly.

    // define the service in the components block
    {name: "preferencesService", kind: "enyo.SystemService"},

    // elsewhere, in create() for example, call getPreferences passing
    // the desired keys
    create:function() {
        this.inherited(arguments);
        this.$.preferencesService.call({
            keys: ["pref1", "pref2"]
        },{
            method: "getPreferences",
            onSuccess: "gotPreferences",
            onFailure: "gotPreferencesFailure"
        });
    }

    // setting the preferences uses a similar call.  you'd execute this
    // on button click, property change, or view change, for example
    buttonClicked:function(source, event) {
        this.$.preferencesService.call({
            "pref1":"value of pref 1",
            "pref2":"value of pref 2"
        },{
            method: "setPreferences",
            onSuccess: "setPreferences",
            onFailure: "setPreferencesFailure"
        });
    }

    That works perfectly fine but is a little verbose for my taste.  There's also an (undocumented) PreferencesService kind you can use.  With that component, you're insulated from the underlying system service and instead call one of its two methods:

    // passing an object of name/value pairs
    updatePreferences(inPreferences)

    // passing an array of key names
    fetchPreferences(inKeys, inSubscribe)

    It also has an onFetchPreferences event to which you can listen to be notified when preferences have been loaded.

    Finally, I've created a new kind as part of my enyo-extras repo called AutoPreferencesService.  This component works by inspecting the published properties of its sub-kind and automatically loading those preferences and auto-wiring change events to update preferences.  This allows you to create a app or feature-specific preferences interface that's more semantic while still insulating the app from directly interacting with the system service.

    // create a kind inheriting from AutoPreferencesService
    enyo.kind({
        name:"myApp.ExamplePrefs",
        kind:"extras.AutoPreferencesService",
        published:{  // declare your preferences and default values
            pref1:"",
            pref2:""
        }
    });

    // add the new kind to you apps components block
    {kind:"myApp.ExamplePrefs", onLoad:"prefsReady", name:"prefs"}

    // add onLoad callback handler
    prefsReady:function() {
        var pref1 = this.$.prefs.getPref1();
        // do something useful with pref1
    }

    // update preference as needed
    saveButtonClicked:function() {
       this.$.prefs.setPref1("new value");
    }

    This will be my new default way to deal with preferences.  What do you think?  Which approach is your preference or are you dealing with preferences a different way entirely?