tvOS: Create a Search TVML Page with TVML SearchTemplate

This is the TVML SearchTemplate (taken from Apple's Docs: https://developer.apple.com/library/prerelease/tvos/documentation/LanguagesUtilities/Conceptual/ATV_Template_Guide/SearchTemplate.html#//apple_ref/doc/uid/TP40015064-CH33-SW1


var Template = function() {
  return `<?xml version="1.0" encoding="UTF-8" ?>
  <document>
    <head>
      <style>
        .suggestionListLayout {
          margin: -150 0;
        }
      </style>
    </head>
    <searchTemplate id="tmpl_search">
      <searchField/>
      <collectionList>


        <separator></separator>




      </collectionList>
    </searchTemplate>
  </document>`
}



It's something very basic.


When the template is populated by some items it looks like the following


var Template = function() {
  return `<?xml version="1.0" encoding="UTF-8" ?>
  <document>
    <head>
      <style>
        .suggestionListLayout {
          margin: -150 0;
        }
      </style>
    </head>
    <searchTemplate>
      <searchField/>
      <collectionList>
        <shelf>
          <header>
            <title>Shelf Title</title>
          </header>
          <section>
            <lockup>
              <img src="${this.BASEURL}resources/images/movies/movie_520_e1.lcr" width="350" height="520" />
              <title>Title 1</title>
            </lockup>
            <lockup>
              <img src="${this.BASEURL}resources/images/movies/movie_520_e2.lcr" width="350" height="520" />
              <title>Title 2</title>
            </lockup>
            <lockup>
              <img src="${this.BASEURL}resources/images/movies/movie_520_e3.lcr" width="350" height="520" />
              <title>Title 3</title>
            </lockup>
          </section>
        </shelf>
        <grid>
          <header>
            <title>Grid Title</title>
          </header>
          <section>
            <lockup>
              <img src="${this.BASEURL}resources/images/music/music_520_e1.lcr" width="350" height="350" />
              <title>Title 1</title>
            </lockup>
            <lockup>
              <img src="${this.BASEURL}resources/images/music/music_520_e2.lcr" width="350" height="350" />
              <title>Title 2</title>
            </lockup>
            <lockup>
              <img src="${this.BASEURL}resources/images/music/music_520_e3.lcr" width="350" height="350" />
              <title>Title 3</title>
            </lockup>
          </section>
        </grid>
      </collectionList>
    </searchTemplate>
  </document>`
}


Now, as soon as you name the template you should be able to get access to it in your Presenter code, that register events for the new Document:


resourceLoader.loadResource(templateURL, function(resource) {
  if (resource) {
  var doc = self.makeDocument(resource);
  self.currentDOMDocument=doc


  // DOM Document "onload" event
  doc.addEventListener("load", self.dispatch.bind(self));


  // DOM element "select" | "highlight" event
  doc.addEventListener("select", self.load.bind(self));
  doc.addEventListener("highlight", self.load.bind(self));

  if (self[presentation] instanceof Function) {
  self[presentation].call(self, doc, ele);
  } else {
  self.defaultPresenter.call(self, doc);
  }
  }
  }
  )


So, in this case the event should be a "select" event on the search field:


<searchField/>


and so you will be able to get the featured i.e. the keyboard and to attach events for the input like:


var formTemplate = ele.parentNode.parentNode; // your formTemplate button
  var children = formTemplate.childNodes;
  var textField = children.item(1); // replace "1" with the index of "textField" element in your template
  var keyboard = textField.getFeature("Keyboard"); // get the textField's Keyboard element



and the input text


var userValue = keyboard.text.replace(/-/g,""); // the value entered by the user on the Apple TV keyboard


At the "load" event you can attach events to get input changes like:


var children = ele.childNodes;
  var textField = children.item(1); // replace "1" with the index of "textField" element in your template
  var keyboard = textField.getFeature("Keyboard"); // get the textField's Keyboard element
  keyboard.onTextChange = function () {
  console.log("onTextChange "+keyboard.text)
  if( keyboard.text.length && !((keyboard.text.length+1) % 5) ) { // every 4 chars
  keyboard.text+='-';
  }
  }
  }


At this time you have the query string for your Search API.

As soon as you get a callback from your API (that could make use of sayts - Search As You Type),

we need to dynamically add items to the search results as new


<shelf/>


and inside a list of


<section><lockup/></section>


lockup items. So how to dynamically add now these elements?

First off you will need a reference to the parsed document. (I keep a reference to the current view in a service which is also responsible also parses my xml strings) From my API I get a list of results. Looping through them I add to an xml string and insert it:

var searchResults = builder.getCurrentView().getElementById("searchResults").innerHTML = that.resultsString;

So you suggest to write the whole innerHTML string like


Presenter.searchCollectionList.innerHTML='<shelf><header><title>My List</title></header><section><lockup><img src="http://s.mxmcdn.net/images-storage/albums8/0/8/6/4/9/4/11494680_350_350.jpg" width="326" height="326"/><title>Innuendo</title><subtitle>Queen</subtitle></lockup></section></shelf>'


Using your approach, it's very easy since, as soon you get your <lockup/> items, you can append them to the <shelf/> string and then replace the innerHTML. But is this more efficient to traverse the XML DOM nodes so something like?


lastEl = myElements[myElements.length - 1];
for (i = myElements.length; i < l; i++) {
                el = children.item(0);
                container.insertBefore(el, lastEl.nextSibling);
                lastEl = element;
            }


Said that, it's a bit trick to traverse the DOM with for-loops etc and of course replacing the whole innerHTML makes the trick anyways.

Are you guys having a problem with the app crashing when using search after rendering dynamic results?

So, in my understanding


  • The Document.innerHTML solution is quick and dirty, and it just works as expected: DOM elements are replaced all together, and are selectable, focusable as showed above; I'm not sure if this is the most efficient solution, since the VM is replacing the whole structure of the DOM, rebuilding that from scratch;
  • Another solution, that could be more feasibile programmitically could be traversing the DOM nodes, and building node by node and appending the node to the other, but we need some Helpers to do that:


A remove child nodes function, to remove all children of a parent node:

/*
  * Remove all children nodes
  */
  removeAllChildren : function(document) {
  while( document.firstChild ) {
  document.removeChild( document.firstChild );
  }
  },



A serialize function to create the string from a DOM element, very useful for debugging purposes and to serialize/deserialize the nodes to append:


/*
  * Serialize a XML Document to String
  */
  serialize : function(document) {
  if(!Presenter.serializer) {
  Presenter.serializer = new XMLSerializer();
  }
  var str=Presenter.serializer.serializeToString(document);
  return str;
  },



A deserialize or parse function that does exactly the opposite of the previous one:


/*
  * Parse document string to DOM
  */
  parse : function (XMLString, document) {
  if (!Presenter.parser) {
  Presenter.parser = new DOMParser();
  }
        var node = Presenter.parser.parseFromString(XMLString, 'text/xml');
        if (!document) { return node; }
        return document.adoptNode(node.documentElement);
    },



It's important to understand that the document must adopt the node, otherwise you will not able to append it to the same document (the virtual machine will raise an exception about wrong parent node / child node not belonging to the document)


So if you do serialize ( parse () ):


> Presenter.serialize( Presenter.parse("<section></section>", Presenter.searchCollectionList.ownerDocument) )
< "<section itmlID=\"id_34\"/>" = $24


you will get the starting DOM node.


At this point we can do everything from scratch (you can test it out in the JSContext inspector of Safari:


We have a collection list template as root:


> Presenter.searchCollectionList.nodeName
< "collectionList" = $25


We first create the root node of our first Shelf to append, without any <lockup/> item within


shelfRootItem=Presenter.parse("<shelf><header><title>Tracks</title></header></shelf>", Presenter.searchCollectionList.ownerDocument)


At this point we create the <section/>


shelfSectionItem=Presenter.parse("<section></section>", Presenter.searchCollectionList.ownerDocument)


Now we create the <lockup/> items to append to the <section/> item. This will be as many as are search results are for this <shelf/>:

shelfLockupItem=Presenter.parse("<lockup><img src=\"\" width=\"326\" height=\"326\"/><title>Innuendo</title><subtitle>Queen</subtitle></lockup>",shelfRootItem.ownerDocument)


NOTE. The Presenter.parse(string,documentOwner) document parameter is now shelfRootItem.ownerDocument since the document root of this node has changed of course.


Ok, now check what we have done:


> Presenter.serialize(shelfRootItem)
< "<shelf itmlID=\"id_23\">
  <header>
    <title>Tracks</title>
  </header>
</shelf>" = $18

> Presenter.serialize(shelfSectionItem)
< "<section itmlID=\"id_24\"/>" = $19

> Presenter.serialize(shelfLockupItem)
< "<lockup itmlID=\"id_25\">
  <img src=\"/\" width=\"326\" height=\"326\"/>
  <title>Innuendo</title>
  <subtitle>Queen</subtitle>
</lockup>" = $20


Ok they are as expected, so let's append them. We usually do in DOM this in the reverse order:


We append the <lockup/> to the <section/>. Please iterate this over your elements.

NOTE. Do not try to append the same node (having the same itmlID), this will be dropped. You need to create a new node:


> shelfSectionItem.appendChild(shelfLockupItem)
< IKDOMElement {} = $17
> Presenter.serialize(shelfSectionItem)
< "<section itmlID=\"id_24\">
  <lockup itmlID=\"id_25\">
    <img src=\"\" width=\"326\" height=\"326\"/>
    <title>Innuendo</title>
    <subtitle>Queen</subtitle>
  </lockup>
</section>" = $21


Ok, we now append the <section/> to the very <shelf/> node


> shelfRootItem.appendChild(shelfSectionItem)
< IKDOMElement {} = $16
> Presenter.serialize(shelfRootItem)
< "<shelf itmlID=\"id_23\">
  <header>
    <title>Tracks</title>
  </header>
  <section itmlID=\"id_24\">
    <lockup itmlID=\"id_25\">
      <img src=\"g\" width=\"326\" height=\"326\"/>
      <title>Innuendo</title>
      <subtitle>Queen</subtitle>
    </lockup>
  </section>
</shelf>" = $22


As we can see by Presenter.serialize(), the shelf is ready to be appended to the <collectionList/> template document, so let's go:


Presenter.searchCollectionList.appendChild(shelfRootItem)


This will show up the item in the User Interface i.e. in your TVML search results page. And we can check it out:


> Presenter.serialize(Presenter.searchCollectionList)
< "<collectionList itmlID=\"id_8\">
  <separator itmlID=\"id_9\"/>
  <shelf itmlID=\"id_11\"/>
  <shelf itmlID=\"id_23\">
    <header itmlID=\"id_26\">
      <title itmlID=\"id_27\">Tracks</title>
    </header>
    <section itmlID=\"id_24\">
      <lockup itmlID=\"id_25\">
        <img src=\"\" width=\"326\" height=\"326\" itmlID=\"id_29\"/>
        <title itmlID=\"id_30\">Innuendo</title>
        <subtitle itmlID=\"id_31\">Queen</subtitle>
      </lockup>
    </section>
  </shelf>
</collectionList>" = $23


We can see the child nodes:


> Presenter.searchCollectionList.childNodes.length
< 3 = $26



the DOM has a valid structure. For debugging purposes, having the references of all the DOM objects we can then create and add another node:


> shelfAnotherLockupItem=Presenter.parse("<lockurcp\" width=\"326\" height=\"326\"/><title>Innuendo</title><subtitle>Queen</subtitle></lockup>",shelfRootItem.ownerDocument)
< IKDOMElement {} = $27
> shelfAnotherSectionItem=Presenter.serialize( Presenter.parse("<section></section>", Presenter.searchCollectionList.ownerDocument) )
< "<section itmlID=\"id_36\"/>" = $28
> shelfAnotherSectionItem=Presenter.parse("<section></section>", Presenter.searchCollectionList.ownerDocument)
< IKDOMElement {} = $29
> shelfAnotherSectionItem.appendChild(shelfAnotherLockupItem)
< IKDOMElement {} = $27
> shelfRootItem.appendChild(shelfAnotherSectionItem)
< IKDOMElement {} = $29


A brand new <lockup/> item (with different itmlID then) will be added to the search result page i.e. the <searchCollectionList/>.

I was having a lot of trouble with crashes down inside UICollectionView but code was in Javascript interacting with a <section> in a searchTemplate. The code was dynamically adding and removing nodes (as described above) from a <section>, inside a <grid>, that was inside a <collectionList>. I changed the code to add/remove nodes from the <collectionList> instead and crashing stopped.

tvOS: Create a Search TVML Page with TVML SearchTemplate
 
 
Q