Ember.js ajax autocomplete

Purpose of this page is to show my solution for a basic ajax crafted autocomplete service. Since I ran into 1001 problems, I assume this may help others, too.

Especially if you ran into this error message:

Uncaught Error: Assertion Failed: The value that #each loops over must be an Array.

The reason was quite obvious. I didn’t have an array, what I got was the one object that find() returns. You will see below.

I found these videos helpful, to get a better feeling for how Ember.js works under the hood:

I will not explain more than necessary to the code. Please refer to the videos for introductory advices.

This is my assumed ajax service output:

{"findwords": [
  {
    "id": 1,
    "simplified": "你好",
    "traditional": "你好",
    "pinyin": "ni3hao3",
    "english": "Hallo"
  },
  {
    "id": 2,
    "simplified": "对",
    "traditional": "對",
    "pinyin": "dui4",
    "english": "Correct"
  }
]}

Notes:

  • The JSON output must be fully properly quoted. If your promise gets rejected, you might have a JSON syntax issue (or a structure issue).
  • The model identifier (here “findwords”) must be the result object property that holds the actual result.
  • The example makes use of ember-data.

This is the HTML template file:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Autocomplete Example</title>
  <link rel="stylesheet" href="css/libs/bootstrap.min.css">
  <link rel="stylesheet" href="css/libs/bootstrap-theme.min.css">
  <link rel="stylesheet" href="css/style.css">
  <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1" />
  <script src="js/libs/jquery-1.10.2.js"></script>
  <script src="js/libs/handlebars-1.1.2.js"></script>
  <script src="js/libs/ember-1.5.1.js"></script>
  <script src="js/libs/ember-data.js"></script>
  <script src="js/libs/bootstrap.min.js"></script>
  <script>
</head>
<body>
  <script type="text/x-handlebars">
        {{outlet}}
  </script>

  <script type="text/x-handlebars" id="autocomplete">
    {{input type="text" value=phrase placeholder="Add words to your drill"}}
    <ul>
      {{#each searchResults}}
        <li>{{this.pinyin}} - {{this.english}}</li>
      {{/each}}
    </ul>
  </script>

  <script type="text/x-handlebars" id="form">
    {{render "autocomplete"}}
  </script>
</body>
</html>

The input field is connected to the “phrase” attribute of the controller.


App = Ember.Application.create();

// Model
App.Findword = App.Findwords = DS.Model.extend({
  simplified: DS.attr(),
  traditional: DS.attr(),
  pinyin: DS.attr(),
  english: DS.attr()
});

// Routes
App.Router.map(function() {
  this.route("form", { path: "/" });
  //this.route("findwords", { path: "findwords/:phrase" });
  this.route("findwords", { path: "findwords" });
});

The application needs to know the data structure of the JSON objects – the model for it.
I created “Findword” and “Findwords” because I found Ember.js requesting both, even though only “Findwords” is specified
I first assumed it’s a good way to have the “:phrase” attached to the path, but found out later that when searching … URL parameters are more convenient.


// The controller
App.AutocompleteController = Ember.ArrayController.extend({
  phrase: '',
  searchResults: function() {
    var searchText = this.get('phrase');
    if(searchText.length > 0) {
      return this.store.find("findwords", {"phrase": searchText});
    }
  }.property("phrase")
});

The controller has a property “searchResult”, which you find above in the HTML file. It provides the search result.
As soon as something is entered in the input field, the method loadData is fired, filling the search result.

Notes:

  • find() makes an ajax call but only returns one item from the list – if an id is specified.
  • findAll() does not accept parameters
  • filter() does not make an ajax call
  • Returning the promise of a find(type,id) call directly, results in an error telling #each cannot iterate over a Promise, but expects an array.

find() is the way to go, but only with parameters in form of an object.

That’s basically it.

I had some trouble debugging the “#each/Promise” issue and landed at an also working solution. It indirectly fills the “searchResult” with objects from the promise. That could maybe serve in other cases, so here it comes:


// The controller
App.AutocompleteController = Ember.ArrayController.extend({
  phrase: '',
  loadData: function() {
    var searchText = this.get("phrase");
    var controller = this;
    if(searchText.length > 0) {
    this.set("searchResults", Ember.A() );
    this.store.find("findwords", {"phrase": searchText}).then(
      function(data) {
        controller.get('searchResults').addObjects( data );
      },
      function(data) {
        alert('rejected, probably syntax error!');
      }
    );
    }
  }.observes("phrase"),
  searchResults: Ember.A()
});

Meine erste Woche in Hsin-Chu, Taiwan

Diese Geschichte geht vom Samstag meiner Ankunft bis zu darauf folgenden Samstag. Zukünftig fasse ich mich kürzer. :-)

Durch die Umbuchung des Fluges war ich sehr spät erst in Taipeh. Aus dem Flugzeug raus war mir so übel … ich war total übermüdet. War mir gar nicht sicher wie ich es bis zur Universität schaffen soll.

Die “Immigration” nach Taiwan dauerte mehr als eine Stunde, die Schlange war brutal lang. Überall muss man diese Identitäts-Zettel ausfüllen. Beim Abreisen und beim Ankommen. Immer.

Der Plan von der Uni war glücklicherweise ausreichend gut, so dass ich mit ein bisschen rumfragen auch am Kuang-Fu Campus angekommen bin.
Das war leider der falsche Campus. Mein Wohnheim ist nämlich in Boai (dem Satelliten-Campus, den Werktags ein Shuttle-Bus mit Kuang-Fu verbindet). Aber ein Student war so nett mich von dort zum anderen Campus zu fahren. Dort durfte ich erstmal Jimmy kennen lernen, den Room Manager und Helfer für alles. Bei ihm gab’s erstmal Schlüssel und Bett-Bezug (auf Umwegen, weil mein Bettzeug irgendwo auf dem mit unbekannten Kuang-Fu Campus lagerte).

Weiterlesen

Peking und die unsichtbare Stadt

Dieser Bericht handelt von meiner Reise nach Peking – auf dem Weg nach Hsin-Chu, Taiwan. In Peking und Shanghai kann man bei bestimmten Flügen in Drittländer einen 72-Stunden Aufenthalt ohne Visum machen. Das habe ich für die Flüge rund um mein Auslandssemester gemacht.

Frankfurt → Peking → Taipeh → Shanghai → Frankfurt

Vor dem Abflug war ich das totale Nervenbündel, mir ging’s gar nicht gut. 10.000 Kilometer … Rückkehr erst wieder in 4 Monaten … da geht einem der Stift irgendwie.

Um auf jede Art Diebstahl vorbereitet zu sein, hatte ich alle erdenklichen Vorkehrungen getroffen … Kopien aller Dokumente an x Stellen und in der Cloud. Wobei letzteres in China ziemlich dumm ist, die Dienste die wir so kennen sind dort nämlich gesperrt. Ansonsten … Trolli mit Vorhängeschloss, Kabelbindedrähte an Rucksackreißverschluß, alles wichtige am Körper, Jacke immer zu.

Weiterlesen

Pinyin Anlaute Spickzettel für Deutsche

Wer Chinesisch lernt, lernt neben den 4/5 Tönen zuerst die Anlaute. Das ist schwierig, denn nur vom Hören kriegt das nicht jeder hin. Auch Lehrer chinesischer Herkunft können nicht immer ausreichend Hilfestellung geben, wie man den richtigen Ton produziert.

Während meinem Chinesisch Kurs 2013 habe ich für die Pinyin Anlaute einen Spickzettel erstellt. Die Aussprache-Umformung ist auf die deutsche Aussprache ausgelegt.

Den Spickzettel darf jeder benutzen und weitergeben, Schüler wie Lehrer. Viel Spaß damit!

Pinyin Betonung - Anlaute

Barcamp Stuttgart – beer for a tweet #bcs6

Stuttgart, MFG, Literaturhaus , 21.9.-22.9.2013, Barcamp-Stuttgart … es ist jedes mal großartig. :) Das Essen von Esskultur war mal wieder mega geil. Pancakes mit Apfelmus FTW! Am Ende des ersten Tages bin ich vor lauter Überversorgung aus allen Nähten geplatzt. Getränke am Abend bekam man umsonst … naja, man musste einen Tweet absetzen. Blöd, wer da keinen Twitter Account hat … aber auch um das zu lernen, kann ein Barcamp helfen. :)

Ein Highlight bildete die Vor-Wahl aller Teilnehmer (am Sonntag war tatsächlich Bundestagswahl). Wahlbeteiligung 70%: Piraten 34%, Grüne 29%, SPD 10% CDU 8%, Linke 8%, FDP 4%. Die Tendenz ist klar. Die NDP (tatsächlich so auf den Zettel geschrieben) hat auch eine Stimme erhalten.

Die Orga fand ICH persönlich toll (Danke Jan!), auch wenn es seltsam viele kritische Stimmen am Ende gab was insb Slot-Belegung während der Mittagszeit betraf.

Von den bestimmt über 100 Sessions in 10 Tracks konnte ich leider nur 8 mitnehmen. Es folgt die Recap. Weiterlesen

Inkscape Mockup Machine

Mockups (bzw Wireframes, Klickdummies, Demos, Scribbles, Screens oder wie man es auch nennen mag) mit Inkscape zu machen ist auf der einen Seite cool, weil SVG einfach toll ist… auf der anderen Seite unterstützt es den Mockup-Workflow mal garnicht (gut).

Für Mockups – also Screens einer Applikation (die es noch nicht gibt), die eine Abfolge von Aktionen darstellt – verwende ich Ebenen.
Da gibt es Ebenen, die immer da sind und welche, die ausgewechselt werden … und jetzt hat man so 20 Screens zusammengestellt.
Um das Ganze vorführen zu können, müssen in aller Regel Bilder generiert werden … also jede Konstellation einmal. Mega-ätzend. Spätestens, wenn man wegen einer elementaren Änderungen nochmal alles exportieren muss, kriegt man Suizidgedanken.

Aber in Inscape kann man ja mit Python scripten … da muss es doch was geben … hab ich gedacht. :-\

Da ich nichts funktionierendes gefunden habe, das mir diesen Monkey-Job abnimmt, habe ich eine Inkscape Extension entwickelt, die diese Arbeitsweise unterstützt.

Mehr bei GitHub:

» Inkscape Mockup Machine bei GitHub

Drittes TYPO3 Camp Stuttgart – ich war dabei

Auch dieses Jahr organisierte die TYPO3 Community ein Barcamp im Schloss Hohenheim. Großartige Location. :)

Wir von 21TORR sind Freitag zum Aufbau angerückt, konnten aber leider nicht lange an der Kennenlern-Party teilhaben. Die Pizza ist uns leider raus. Aber Gerüchten zufolge war die auch ungefähr so schnell vergriffen wie die Karten zur Apple DevCon. ;)

Bei diesem Camp stand der aktive “Austausch” im Vordergrund. Es gab gefühlt wesentlich mehr Workshops und Diskussionsrunden, als auf den Camps, die ich bisher besucht habe. Mal was anderes, aber auch gut.

Gleich zum Start durfte ich meinen Workshop zu “Internet Sicherheit” (ein bisschen Hacken) halten und habe hoffentlich den einen oder anderen in den Grundfesten schockiert. Aber auch ich habe es als offene Diskussion gehalten, um grade zu diesem Thema aus dem TYPO3-Bereich Erfahrungswerte zu bekommen.
Weiterlesen

Wikipedia Lesbarkeit auf Widescreen

Wikipedia passt sich der Bildschirmbreite an. Das ist eigentlich cool, nur bei breiten Monitoren verschlechtert das die Lesbarkeit.

Hat man einen Wikipedia Account, kann man in seinen Einstellungen “Benutzerdefiniertes CSS” einsetzen und Wikipedia generell schmaler machen.

Mein benutzerdefiniertes CSS für einen maximal 1024 Pixel breiten Inhalt sieht wie folgt aus:

body, #mw-head {
  max-width: 1200px;
  position:relative;
  margin: 0 auto;
}
#content {
  max-width: 1024px;
}

Das muss man leider in jeder Sprache einzeln machen.

Deutsch
» Einstellungen -> Ausssehen -> Benutzeroberfläche > Dein Theme / Benutzerdefiniertes CSS
Englisch
» Preferences-> Appearance-> Skin > Your Theme / Custom CSS

TYPO3 FAL in einer eigenen Extbase/Fluid Extension einsetzen

Mittlerweile klappt das mit dem File Abstraction Layer (FAL) immer besser.

Und wie es klappt, folgt nun. Ziel ist, zwei FAL Felder in eine eigene Extension zu integrieren und alle nötigen Schritte bis zur Ausgabe im Fluid Template zu zeigen.

Die Extension im Beispiel heißt “falusage” und die Tabelle bzw das Model heißt “Root”.

  • Es wird von TYPO3 6.1 ausgegangen.
  • Es wird von einer Extension ausgegangen, die mit dem Extension Builder erstellt wurde. Dadurch bereits vorhandene Konfigurationen werden nicht erklärt.
  • Im Extension Builder wurden am Model “Root” zwei Felder “image” (ein Bild) und “files” (Dateien) konfiguriert und der Controller beherrscht eine List-Action.

Datei ext_tables.sql

CREATE TABLE tx_falusage_domain_model_root (
	# ...
	image varchar(255) DEFAULT '' NOT NULL,
	files varchar(255) DEFAULT '' NOT NULL,
	# ...
);

Datei Configuration/TCA/Root.php

$TCA['tx_falusage_domain_model_root'] = array(
	// ...
	'image' => array(
		'exclude' => 1,
		'label' => 'Image',
		'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig('image', array(
			'appearance' => array(
				'createNewRelationLinkTitle' => 'LLL:EXT:cms/locallang_ttc.xlf:images.addFileReference'
			),
			'minitems' => 0,
			'maxitems' => 1,
		), $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']),
	),
	'files' => array(
		'exclude' => 1,
		'label' => 'Files',
		'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig('files', array(
			'appearance' => array(
				'createNewRelationLinkTitle' => 'LLL:EXT:cms/locallang_ttc.xlf:images.addFileReference'
			),
		), $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']),
	),
	// ...
);

An getFileFieldTCAConfig wird der aktuelle Feldname übergeben. Die Funktion erzeugt einen großen Schwung Konfiguration.

Datei Classes/Domain/Model/Root.php

class Root extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity {
	// ...
	
	/**
	 * Image
	 * @var \TYPO3\CMS\Extbase\Domain\Model\FileReference
	 */
	protected $image;

	/**
	 * Files
	 * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference>
	 */
	protected $files;
	
	// ...
	
	/**
	 * Returns the image
	 *
	 * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> $image
	 */
	public function getImage() {
		return $this->image;
	}

	/**
	 * Sets the image
	 *
	 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> $image
	 * @return void
	 */
	public function setImage($image) {
		$this->image = $image;
	}

	/**
	 * Returns the files
	 *
	 * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> $files
	 */
	public function getFiles() {
		return $this->files;
	}

	/**
	 * Sets the files
	 *
	 * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\TYPO3\CMS\Extbase\Domain\Model\FileReference> $files
	 * @return void
	 */
	public function setFiles($files) {
		$this->files = $files;
	}
	
	// ...
}

“\TYPO3\CMS\Extbase\Domain\Model\FileReference” als Typ ist wichtig, “File” funktioniert wider erwarten nicht und zu ganz wilden Fehlern.

Datei Classes/Controller/RootController.php

class RootController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController {
	// ..
	public function listAction() {
		$roots = $this->rootRepository->findAll();
		$this->view->assign('roots', $roots);		
	}
	// ..
}

Datei Resources/Private/Templates/Root/List.html

Es folgen eine ganze Reihe Fluid Beispiel; wie man an welche Information rankommt.

Mein Model:                       root
Das FileReference Model:          root.image
Der sys_file_reference Datensatz: root.image.originalResource
Der sys_file Datensatz:           root.image.originalResource.originalFile

<f:for each="{roots}" as="root">
Orginal-Attribute:<br />
Dateiname:    {root.image.originalResource.originalFile.name}<br />
Titel:        {root.image.originalResource.originalFile.title}<br />
Beschreibung: {root.image.originalResource.originalFile.description}<br />
Alt-Text:     {root.image.originalResource.originalFile.alternative}<br />
UID:          {root.image.originalResource.originalFile.uid}<br />
Pfad:         {root.image.originalResource.publicUrl}<br /><br />

Referenz-Attribute:<br />
Titel:        {root.image.originalResource.title}<br />
Beschreibung: {root.image.originalResource.name}<br /><br />

Ausgabe als Bild:<br />
<f:image src="{root.image.originalResource.originalFile.uid}" alt="" /><br />
<f:image src="{root.image.uid}" alt="" treatIdAsReference="TRUE" /><br /><br />

Als Link:<br />
<a href="{file.originalResource.publicUrl}">{file.originalResource.title}</a><br /><br />

<f:for each="{root.files}" as="file">				
	<p>{file.originalResource.originalFile.title}</p>
</f:for>

</f:for>

Für Typoscript:

Für Typoscript gibt es hier ein paar Beispiele, aber keine Garantie ob insb. Forenbeiträge aktuell sind.