Posts

  • Enyo Daily #18 - Dynamic depends.js

    While it's probably not fair to call this "daily" any longer, I'm going to stick with it anyway.

    You may have heard that enyo has found its way onto the legacy devices via a new Maps application in the App Catalog.  To test things out, I tried to deploy Score Keeper onto my Pre2.  It deploys successfully but there are definitely some bugs to sort out.  One way I intend to clean up the UI is by including a phone-specific stylesheet through a dynamic depends.js file.

    [[MORE]]

    The normal depends.js file looks something like this:

    enyo.depends(
        "controls.js",
        "app.js",
        "app.css"
    );

    This tells enyo to load up controls.js, app.js, and app.css in the same directory as this depends.js file.  But depends.js is just a regular javascript file so there's nothing preventing you from doing something a little more robust here to select which files you want to include.  Also, since the framework is the one including depends.js in the first place, all the framework functions are available during execution of depends.js.

    I elected to use a closure to keep the global namespace clear and base my custom CSS inclusion on the screen width.  If you're aware of a better condition to check, please share in the comments.  Inside the function, I retrieve the device info and build an array of the common files.  Next, I check the device width and push the extra css if appropriate.  Finally, I call enyo.depends (via Function.apply since I'm working with an array) and that's all!

    (function() {
        var di = enyo.fetchDeviceInfo();
       
        var paths = ["extras.js",
                     "views.js",
                     "app.css"];
       
        if(di && di.screenWidth < 500) {
            paths.push("app-phone.css");
        }
       
        enyo.depends.apply(enyo, paths);
    })();

  • Enyo Daily #17 - Events - Part 5

    I wouldn't have expected to write so much about events in enyo but there's a lot of depth to cover.  I previously posted about an Animated Grid Layout control I created which I later used in Score Keeper for the board selection.  I have a similar requirement for the main panes of InContact but also need to reorder the items.[[MORE]]

    To enable the reordering, I wanted to defer the event handling to the new grid control rather than binding them in my app.  This keeps that logic contained to a separate control (one I could swap out for an alternate grid control if desired) and not muddling up my main app code.  The rub with this is that is was difficult to add an event handler to an enyo instance when you're not the owner of the control (in my scenario, the main app is the owner whereas the grid is just the container.  More on ownership and containment in Enyo Daily 3).

    The rather simple solution I discovered was to add an instance function to the enyo instance during creation.  The function is bound (using enyo.bind) to the grid control so it can enable reordering.

    One caution with this approach is that it will by default prevent calling a handler declared for the onX event for the instance.  You could call dispatchIndirectly yourself in your instance method but I'll leave that as homework.  :)

    Here's a quick example.  I'll share the reorderable grid control once it's actually complete!

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"ApplicationEvents", name:"appEvents", onUnload:"logAppEvent"},
            {kind:"EventSender", name:"eventSender", onSend:"sent"},
            {kind:"Button", onclick:"clicked"},
            {kind:"ContainerListener", components:[
                {kind:"Button", caption:"Clicking me will be handled by my container"}
            ]},
            {kind:"Input", onkeypress:"press"},
            {kind:"Dashboard", name:"db"}
        ],
        create:function() {
            this.inherited(arguments);
           
            this.$.db.setLayers([{title:"My Dashboard", text:"important message"}]);
           
            // 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";
            });
           
            enyo.dispatcher.rootHandler.addListener(this);
        },
        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)
        },
        customGlobalEventHandler:function(sender, param) {
            enyo.log("customGlobalEvent", param.data);
        }
    }

    var _ContainerListener = {
        name:"ContainerListener",
        kind:"Control",
        createComponent:function() {
            var o = this.inherited(arguments);
            o.clickHandler = enyo.bind(this, "childClicked");
            return o;
        },
        childClicked:function(source, event) {
            enyo.log("one of my child nodes was clicked!")
        }
    }

    var _EventSender = {
        name:"EventSender",
        kind:"Component",
        events:{
            onSend:"handleOnSend",
            onAlert:{value:"handleOnAlert", caller:"sendAlert"}
        },
        create:function() {
            this.inherited(arguments);
           
            enyo.dispatcher.rootHandler.addListener(this);
        },
        go:function() {
            this.doSend(1,2,3);    // dispatchIndirectly
            this.sendAlert({a:1, b:2});
            window.enyo.dispatch({type:"customGlobalEvent", data:"something to pass"});
        },
        customGlobalEventHandler:function(sender, e) {
            enyo.log("also caught customGlobalEvent");
        }
    }

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

  • Scorekeeper

    Score Keeper

    Had to take some time away from my Enyo Daily series to actually finish up my first Touchpad app!!  It's called Score Keeper and, as the name implies, it helps you keep track of the score for a game or activity.  I just submitted it to the catalog and should be available soon.  You'll find it here once it's available.

  • Enyo Daily #16 - Events - Part 4

    A question came up on the forums regarding global custom events.  The use case described was the need to pass an event up or down the object hierarchy just to react to it at the appropriate level.  Enyo actually can support this with the current architecture using its event dispatcher and it only requires a couple lines of code to implement.[[MORE]]

    To fire the event, use window.enyo.dispatch(eventObject). eventObject must define a type member.

    window.enyo.dispatch({type:"customGlobalEvent", data:"something to pass"});

    To catch the event, add your control to the rootHandler's listeners and implement a xxxHandler method for the event.

    create:function() {
      this.inherited(arguments);
      enyo.dispatcher.rootHandler.addListener(this);
    },
    customGlobalEventHandler:function(sender, param) {
      enyo.log("customGlobalEvent", param.data);  // logs: customGlobalEvent something to pass
    }

    Here it is in action in my continuing events example:

    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";
            });
           
            enyo.dispatcher.rootHandler.addListener(this);
        },
        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)
        },
        customGlobalEventHandler:function(sender, param) {
            enyo.log("customGlobalEvent", param.data);
        }
    }

    var _EventSender = {
        name:"EventSender",
        kind:"Component",
        events:{
            onSend:"handleOnSend",
            onAlert:{value:"handleOnAlert", caller:"sendAlert"}
        },
        create:function() {
            this.inherited(arguments);
           
            enyo.dispatcher.rootHandler.addListener(this);
        },
        go:function() {
            this.doSend(1,2,3);    // dispatchIndirectly
            this.sendAlert({a:1, b:2});
            window.enyo.dispatch({type:"customGlobalEvent", data:"something to pass"});
        },
        customGlobalEventHandler:function(sender, e) {
            enyo.log("also caught customGlobalEvent");
        }
    }

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

  • Enyo Daily #15 - Canvas

    Today's been an interesting day for webOS.  If you're a webOS developer (which is probably a safe assumption if you're reading this), you're probably already aware of HP's decision to jettison its hardware division.  As a result, there are in effect no webOS phones or tablets to be released unless they find a suitable hardware partner.  According to the fine folks over at This Is My Next, HP isn't walking away from webOS.  For now, I'll buy into that and continue to create things for a platform I believe deserves to thrive.

    That said, I've taken time off from posting due to the birth of my daughter (our third child).  As a result, I haven't taken much time to plan out the next couple posts.  But, I wanted to get something out today so I'll cover how to get started with the HTML5 canvas tag in enyo.

    [[MORE]]

    Enyo doesn't provide much for developing in canvas; it doesn't really need to.  Canvas in enyo is the same as in normal web development.  There are a couple minor hurdles you have to overcome, however.

    First, how do you create a <canvas> tag when all enyo controls are defined in JavaScript?  The undocumented nodeTag property, of course!  DomNode includes this property and uses it to determine the string to pass to document.createElement.  So, to create an enyo canvas control, set the nodeTag property to "canvas"!

    {name:"myCanvasTag", nodeTag:"canvas"}

    Second, how do you get a reference to the drawing context to draw on the canvas.  Like other enyo controls, the DOM node doesn't exist until the control is rendered.  The simplest solution is to override rendered() in the control containing the canvas and call your drawing methods there.

    rendered:function() {
        var n = this.$.myCanvasTag.hasNode();
        var ctx = n.getContext("2d");
        ctx.fillRect(0,0,100,100);
    }

    Here's a complete example that creates 5 canvas tags using VirtualRepeater.

    var _App = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {name: "repeater", kind:"VirtualRepeater", onSetupRow:"setupRow", components:[
                {name: "canvas", nodeTag:"canvas"}
            ]}
        ],
        rendered:function() {
            this.inherited(arguments);
           
            for(var i=0;i<5;i++) {
                // this is a protected method so ... palm may not like you using it ...
                this.$.repeater.prepareRow(i);
                var n = this.$.canvas.hasNode();
                n.width = 150;
                n.height = 150;
                n.getContext("2d").strokeText("I am item " + i, 10, 10);
            }
        },
        setupRow:function(source, index) {
            if(index < 5) {
                return true;
            }
        }
    }

    enyo.kind(_App);

  • Enyo Daily #14 - Scroller Fades

    I found it strange that the generic Scroller control didn't have scroll fades; instead you have to use FadeScroller.  Even more troublesome, if you wanted fades on a SnapScroller, for example, the framework didn't provide for that.  The solution is easy fortunately (particularly when you steal the code from FadeScroller!).[[MORE]]

    The code use is directly adapted from FadeScroller.  I had some issues copying it verbatim but the effect is the same.  There are two steps to getting fades working.

    First, you need to create an instance of the ScrollFades kind.  I at first tried adding it as a child of Scroller but the fades wouldn't position correctly.  The cause was that as a child of the scroller client (inner wrapper), the fade would get scroller along with the other content.  The fades really needed to be siblings of the client so creating them as chrome rather than normal components did the trick (More on chrome in a future post).

    Below, I'm creating the ScrollFades component as chrome of the SnapScroller(createChrome = createcomponent with isChrome=true).

    create:function() {
        this.inherited(arguments);
        this.$.scroller.createComponent({kind:"ScrollFades", name:"fades", isChrome:true, owner:this});
    },

    Second, I hook scroll events for the Scroller to the showHideFades method of ScrollFades.  The ScrollFades component takes care of determine which fades to show based on the position of the scroller.

    // in the components block ...
    {kind:"SnapScroller", vertical:false, height:"100px", name:"scroller", onScroll:"scrolled", components:[ /* components */}

    // later on ...
    scrolled:function(sender) {
        this.$.fades.showHideFades(this.$.scroller);
    }

    And that's it!  If you're looking for some homework, make this into a custom FadeSnapScroller control that encapsulates the fades.  Here's the complete example:

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"SnapScroller", vertical:false, height:"100px", name:"scroller", onScroll:"scrolled", components:[
                    {content:"Button 1", style:"margin:10px"},
                    {content:"Button 2", style:"margin:10px"},
                    {content:"Button 3", style:"margin:10px"},
                    {content:"Button 4", style:"margin:10px"},
                    {content:"Button 5", style:"margin:10px"},
                    {content:"Button 6", style:"margin:10px"},
                    {content:"Button 7", style:"margin:10px"},
                    {content:"Button 8", style:"margin:10px"},
                    {content:"Button 9", style:"margin:10px"},
                    {content:"Button 10", style:"margin:10px"},
                    {content:"Button 11", style:"margin:10px"},
                    {content:"Button 12", style:"margin:10px"},
                    {content:"Button 13", style:"margin:10px"},
                    {content:"Button 14", style:"margin:10px"},
                    {content:"Button 15", style:"margin:10px"},
            ]}
        ],
        create:function() {
            this.inherited(arguments);
            this.$.scroller.createComponent({kind:"ScrollFades", name:"fades", isChrome:true, owner:this});
        },
        scrolled:function(sender) {
            this.$.fades.showHideFades(this.$.scroller);
        }
    }

    enyo.kind(_Example);

  • Enyo Daily #13 - Toolbar Controls

    One of the reasons I enjoy working with webOS is its foundation of standard web technologies.  In practice, this translates to the ability to customize the behavior and display of the framework in ways its creators didn't foresee.  One such mechanism is CSS -- parodoxically my most and least favorite.  I love that I can throw a -webkit-border-radius on a <div> rather than opening up a graphics program and the flexible box model is amazing (coming from someone who lived by nested tables back in the day).

    HP has already covered most of the built-in Toolbar controls in their Enyo Developer Guide but there's nothing preventing you from dropping any control you want in a Toolbar.  I'll cover a couple examples and show how reusing existing enyo CSS classes and tweaking others can make other controls look at home in the Toolbar.

    [[MORE]]

    I'll start with covering a couple controls the dev guide leaves out:  ToolInput, ToolInputBox, and ToolSearchInput.  In each case, the controls are identical to their non-Toolbar counterparts with the exception of the CSS classes applied.  In fact, each inherits directly from their respective "normal" control.  I mention them here mainly to illustrate a template for how you might ready other controls (Enyo or custom) to be displayed in a Toolbar.

    To illustrate, I create a new ToolSlider and ToolCustomListSelector control.  ToolSlider inherits from Slider and adds the classes from the ToolInput control.  I also added an extras-tool-slider class to reduce the margins to make it fit better. 

    enyo.kind({
      name:"ToolSlider",
      kind:"Slider",
      className:"extras-tool-slider enyo-input enyo-tool-input"
    });

    ToolCustomListSelector inherits from CustomListSelector (surprising, I know) and adds the classes from ToolButton.  Again, I added an additional class, extras-tool-listselector, to manage the height of the control.

    enyo.kind({
      name:"ToolCustomListSelector",
      kind:"CustomListSelector",
      className:"extras-tool-listselector enyo-tool-button-client enyo-tool-button-captioned"
    });

    Here's everything -- all the built-in controls plus the 2 custom controls above -- in action.

    CSS

    .extras-tool-listselector .enyo-listselector-arrow {
        min-height:20px;
    }

    .extras-tool-slider .enyo-slider-progress {
        margin:10px 5px;
    }

    JavaScript

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {kind:"Toolbar", name:"tb1", pack:"justify", name:"toolbar", components:[
                {kind:"ToolButton", caption:"Button"},
                {kind:"ToolInput", hint:"important stuff"},
                {kind:"ToolSearchInput"},
                {kind:"RadioToolButtonGroup", components:[
                      {label:"First"},
                      {label:"Second"},
                      {label:"Third"},
                ]},
                {kind:"ToolButtonGroup", components:[
                    {kind:"GroupedToolButton", caption:"Button"},
                    {kind:"GroupedToolButton", caption:"Button"},
                    {kind:"GroupedToolButton", caption:"Button"},
                ]},
            ]},
            {kind:"Toolbar", name:"tb2", pack:"justify", components:[
                {kind:"ToolInputBox", width:"300px", components:[
                     {kind:"Input", styled:false, flex:1},
                     {kind: "ToolCustomListSelector", value: 3, items: [
                         {caption: "Google", value: 1},
                         {caption: "Bing", value: 2},
                         {caption: "Alta Vista", value: 3},
                     ]},
                ]},
                {kind: "ToolCustomListSelector", value: 2, items: [
                    {caption: "One", value: 1},
                    {caption: "Two", value: 2},
                    {caption: "Three", value: 3},
                ]},
                {kind:"ToolSlider", maximum:10, minimum:0, value:5, width:"200px"},
            ]}
        ]
    }

    enyo.kind({name:"ToolCustomListSelector", kind:"CustomListSelector", className:"extras-tool-listselector enyo-tool-button-client enyo-tool-button-captioned"})
    enyo.kind({name:"ToolSlider", kind:"Slider", className:"extras-tool-slider enyo-input enyo-tool-input"})
    enyo.kind(_Example);

  • Enyo Daily #12 - 3 Ways to Include Content

    There are several use cases for including large blocks of content into your applications.  A couple common scenarios are to include a basic help system or "About" information.  I'll cover three possible ways to do this today.[[MORE]]

    Here's the HTML I'll use for illustration.  Note that it doesn't include any enyo kinds, only raw HTML.  If you want to include enyo, these solutions won't work for you.

    <div class="help">
      <div class="topic">Getting Started</div>
      <div class="content">
        Here is a quick introduction to imported html into you enyo applications.  You can include <a href="http://developer.palm.com">links to external resources</a> or other random markup as you need.  Normally, you'd have much more content but this will do for illustration.
      </div>
    </div>

    The simplest solution for small blocks of content is the content property from Control (technically DomNodeBuilder) and its children.  Since it's a published property, you can interact with it in the same way as any other property -- either declaratively in the component definition or programmatically via setContent().  You can specify any valid HTML for content and it will be included as the innerHTML of the node.  One caveat here is that HTML is escaped by default; to include the raw HTML, set the allowHtml property to true.

    {content:
    '<div class="help">' +
    '  <div class="topic">Getting Started</div>' +
    '  <div class="content">' +
    '    Here is a quick introduction to imported html into you enyo applications.  You can include <a href="http://developer.palm.com">links to external resources</a> or other random markup as you need.  Normally, you'd have much more content but this will do for illustration.' +
    '  </div>' +
    '</div>'}

    That reasonably supportable for that length of text.  If, however, you had 10 topics, your enyo code would become untenable.  If the text is relatively static, at least static for a particular version of your application, the HtmlContent kind might be a good fit.

    HtmlContent allows you to specify content inside your HTML document (as opposed to the JavaScript document containing your enyo components).  The content is referenced by specifying the id of the node wrapping it in the srcId property of HtmlContent.  One nice feature of HtmlContent you don't get in the other options is a special event handler for links.  So, in addition to "regular" links, you can encode special instructions in the href attribute of anchor tags to execute unique actions.

    index.html

    <!doctype html>
    <html>
    <head>
        <title>technisode example app</title>
        <script src="/dev/enyo/1.0/framework/enyo.js" type="text/javascript"></script>
    </head>
    <body>
    <div id="helpContent">
        <div class="help">
          <div class="topic">Getting Started</div>
          <div class="content">
            Here is a quick introduction to imported html into you enyo applications.  You can include <a href="http://developer.palm.com">links to external resources</a> or other random markup as you need.  Normally, you'd have much more content but this will do for illustration.
          </div>
        </div>
    </div>
    <script type="text/javascript">
        new com.technisode.example.App().renderInto(document.body);
    </script>
    </body>
    </html>

    javascript

    {kind:"HtmlContent", srcId:"helpContent", onLinkClick:"linkClicked"},

    The third option is AjaxContent.  It functions similar to HtmlContent except that it loads the content asynchronously from an external file -- either remotely or locally.  With both the content property and the HtmlContent control, the DOM nodes are available as soon as they are created.  With AjaxContent, the nodes aren't available until the onContentChanged event fires.  Turns out that the DOM nodes aren't quite ready when onContentChanged fires; it fires 1 line earlier.  I assume the reason is to allow you to modify the content before it is inserted.  Regardless, if you want to find or traverse the HTML using DOM methods, you can't do so quite yet.

    A simple solution is to wrap any DOM manipulation calls in an enyo.asynchMethod.  This will allow the current call stack to complete which includes inserting the content.  This method is illustrated in the complete example below.

    {kind:"AjaxContent", name:"ajaxContent", allowHtml:true, url:"http://www.technisode.com/blog/helpContent.html", onContentChanged:"ajaxContentChanged"}

    And finally, all together now ...

    index.html

    <!doctype html>
    <html>
    <head>
        <title>technisode example app</title>
        <script src="/dev/enyo/1.0/framework/enyo.js" type="text/javascript"></script>
    </head>
    <body>
    <div id="helpContent">
        <div class="help">
          <div class="topic">Getting Started</div>
          <div class="content">
            Here is a quick introduction to imported html into you enyo applications.  You can include <a href="http://developer.palm.com">links to external resources</a> or other random markup as you need.  Normally, you'd have much more content but this will do for illustration.
          </div>
        </div>
    </div>
    <script type="text/javascript">
        new com.technisode.example.App().renderInto(document.body);
    </script>
    </body>
    </html>

    content.js

    var _Example = {
        name:"com.technisode.example.App",
        kind:"Control",
        components:[
            {content:
                '<div class="help">' +
                '  <div class="topic">Getting Started</div>' +
                '  <div class="content">' +
                '    Here is a quick introduction to imported html into you enyo applications.  You can include <a href="http://developer.palm.com">links to external resources</a> or other random markup as you need.  Normally, you\'d have much more content but this will do for illustration.' +
                '  </div>' +
                '</div>'},
            {kind:"HtmlContent", srcId:"helpContent", onLinkClick:"linkClicked"},
            {kind:"AjaxContent", name:"ajaxContent", allowHtml:true, url:"http://www.technisode.com/blog/helpContent.html", onContentChanged:"ajaxContentChanged"}
        ],
        linkClicked:function(sender, url) {
            // simple pass-through
            window.open(url);
        },
        ajaxContentChanged:function(sender) {
            enyo.asyncMethod(this, function() {
                enyo.log("loading complete.  feel free to manipulate the content now", sender.content);
                sender.hasNode().querySelectorAll(".topic");  // for example
            });
        }
    }

    enyo.kind(_Example);

  • Enyo Daily #11 - Animated Grid Layout

    I started this post by looking through the API docs for a control I hadn't used yet.  I came across the Grid control with no documentation and thought to myself, "This could be interesting."  Turns out, not that interesting.  That did lead me to something else however:  an animated grid layout.

    [[MORE]]The Grid control uses simple CSS floats to align its children to a grid.  Each child is assigned the enyo-grid-div class which defines the height and width in pixels (64x48 to be exact).  You can add a custom class (to change the dimensions, for example) to each automatically by specifying a cellClass property on the Grid component.  That's about it.  Oh, you also get a drop shadow ...

    I created something a little more robust.  I'll emphasize little as I haven't tried to consider the various use cases yet.  In short, the extras.Grid control publishes height and width properties that define the size of a cell and absolutely positions each child according to that grid.  I added a quick css transition to animate changes to grid size and voilá.  For a little icing, I also added a collapsed property that when set to true, will collapse all the controls to the top left corner.  So, if you wanted to implement an iOS-style photo stack, you could use this as a starting point.

    I'm going to flesh this out a bit more and will soon add it to my enyo extras github repo.  In the mean time, here's the prototype to try out:

    CSS

    .extras-grid {
      position:relative
    }
    .extras-grid > * {
      position:absolute;
      -webkit-transition:all 250ms ease-out;
    }

    JavaScript

    var _Grid = {
        name:"extras.Grid",
        kind:"Control",
        className:"extras-grid",
        published:{
            height:200,
            width:150,

            collapsed:false
        },
        create:function() {
            this.inherited(arguments);
            this.resizeHandler();
        },
        rendered:function() {
            this.inherited(arguments);
            if(!this.dim) {
                this.resizeHandler();
            }
        },
        // iterates children and repositions them
        positionControls:function() {
            var c = this.getControls();
            for(var i=0;i<c.length;i++) {
                this.positionControl(c[i], i);
            }
        },
        // calculates position for a control and applies the style
        positionControl:function(control, index) {
            var colsPerRow = Math.floor(this.dim.width/this.width)
            var top = (this.collapsed) ? 0 : Math.floor(index/colsPerRow)*this.height;
            var left = (this.collapsed) ? 0 : (index%colsPerRow)*this.width;
            control.applyStyle("top", top + "px");
            control.applyStyle("left", left + "px");
        },
        collapsedChanged:function() {
            this.positionControls();
        },
        widthChanged:function() {
            this.positionControls();
        },
        heightChanged:function() {
            this.positionControls();
        },
        // reflows controls when window.resize event fires (e.g. device rotation)
        resizeHandler:function() {
            var n = this.hasNode();
            if(!n) return;

            var s = enyo.dom.getComputedStyle(n);
            this.dim = {width:parseInt(s.width),height:parseInt(s.height)};
            this.positionControls();
        }
    }

    enyo.kind(_Grid);

    var _Example = {
        name:"com.technisode.example.App",
        kind:"extras.Grid",
        cellClass:"button",
        components:[{kind:"Button", caption:"Collapse", onclick:"toggle"},
                    {kind:"Button", caption:"Small Grid", onclick:"smallGrid"},
                    {kind:"Button", caption:"Big Grid", onclick:"bigGrid"},
                    {kind:"Button", caption:"Normal Grid", onclick:"reset"},
                    {kind:"Button", caption:"Button 5", onclick:"toggle"},
                    {kind:"Button", caption:"Button 6", onclick:"toggle"},
                    {kind:"Button", caption:"Button 7", onclick:"toggle"},
                    {kind:"Button", caption:"Button 8", onclick:"toggle"},
                    {kind:"Button", caption:"Button 9", onclick:"toggle"}],
        toggle:function() {
            this.setCollapsed(!this.getCollapsed());
        },
        smallGrid:function() {
            this.setWidth(96);
            this.setHeight(72);
        },
        bigGrid:function() {
            this.setWidth(320);
            this.setHeight(240);
        },
        reset:function() {
            this.setWidth(200);
            this.setWidth(150);
        }
    }

    enyo.kind(_Example);

  • Enyo Daily #10 - 7 Line Dashboard App

    Took a couple days off from this series due to a hackathon event I attended.  After writing code for that many hours, I wasn't up to inventing and composing a blog post.  I did have a chance to build an app using jQuery Mobile so perhaps I'll post about that experience sometime in the future.

    Back on topic, I have been intending to investigate dashboards in Enyo for a while but hadn't found the time.  I'll go deeper in a future post but wanted to share a novelty app I created in 7 lines of code.

    The app doesn't do much, as you might expect in 7 lines of javascript, but it is functional.  To be fair, I'm not counting the boilerplate HTML markup so 7 lines might be a little generous.  Regardless, the app launches directly into a dashboard and increments a counter every time it's clicked.  Exciting, eh?

    Without further ado, here it is!

    <!doctype html>
    <html>
    <head>
        <title>Dashboard</title>
        <script src="/dev/enyo/1.0/framework/enyo.js" type="text/javascript"></script>
    </head>
    <body>
    <script type="text/javascript">
        (function () {
            var i = 0, d = enyo.create({kind:"Dashboard", name:"counter"});
            d.clickHandler = function() {
                d.setLayers([{title:"Click Count",text:"I've been clicked " + (i++) + " times"}]);
            }
            d.clickHandler();
        })();
    </script>
    </body>
    </html>

    If you try this at home, be sure to add "noWindow":true to your appinfo.json.