Leggere file locali con JavaScript

http://www.html5rocks.com/en/tutorials/file/dndfiles/?redirect_from_locale=it

Le specifiche forniscono diverse interfacce per accedere ai file locali:

  • File – an individual file; provides readonly information such as name, file size, mimetype, and a reference to the file handle. Riferimento MDN: File()
  • FileList – an array-like sequence of File objects. (Think or dragging a directory of files from the desktop). Riferimento MDN: FileList()
  • Blob – Allows for slicing a file into byte ranges.

Vedere inoltre i riferimenti MDN di: DataTransfer() e FileReader().
Usare file da web app: Tutorial MDN.

Verifica del supporto da parte del browser:

// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
  // Great success! All the File APIs are supported.
} else {
  alert('The File APIs are not fully supported in this browser.');
}

Usare un form per scegliere i file:

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', escape(f.name), '</strong> (',
                   f.type || 'n/a', ') - ', f.size, ' bytes,
                   last modified: ', f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a',
                  '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }
document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Usare il drag&drop:

<div id="drop_zone">Drop files here</div>
<output id="list"></output>
<script>
  function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.dataTransfer.files; // FileList object.

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate ? f.lastModifiedDate.toLocaleDateString() : 'n/a',
                  '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  function handleDragOver(evt) {
    evt.stopPropagation();
    evt.preventDefault();
    evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
  }

  // Setup the dnd listeners.
  var dropZone = document.getElementById('drop_zone');
  dropZone.addEventListener('dragover', handleDragOver, false);
  dropZone.addEventListener('drop', handleFileSelect, false);
</script>

In entrambi i casi si ottiene una FileList() di oggetti File().
Con FileReader() si può leggere un file:

  • FileReader.readAsBinaryString(Blob|File) – The result property will contain the file/blob’s data as a binary string. Every byte is represented by an integer in the range [0..255].
  • FileReader.readAsText(Blob|File, opt_encoding) – The result property will contain the file/blob’s data as a text string. By default the string is decoded as ‘UTF-8’. Use the optional encoding parameter can specify a different format.
  • FileReader.readAsDataURL(Blob|File) – The result property will contain the file/blob’s data encoded as a data URL.
  • FileReader.readAsArrayBuffer(Blob|File) – The result property will contain the file/blob’s data as an ArrayBuffer object.

Chiamando questi metodi, si ottiene una lettura asincrona, con gli eventi onloadstart, onprogress, onload, onabort, onerror, e onloadend.

Esempio: carichiamo immagini da una selezione utente, chiamiamo reader.readAsDataURL() sulle immagini, disegniamo una thumbnail usando l’ attributo ‘src’ con la URL.

<style>
  .thumb {
    height: 75px;
    border: 1px solid #000;
    margin: 10px 5px 0 0;
  }
</style>

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // Loop through the FileList and render image files as thumbnails.
    for (var i = 0, f; f = files[i]; i++) {

      // Only process image files.
      if (!f.type.match('image.*')) {
        continue;
      }

      var reader = new FileReader();

      // Closure to capture the file information.
      reader.onload = (function(theFile) {
        return function(e) {
          // Render thumbnail.
          var span = document.createElement('span');
          span.innerHTML = ['<img class="thumb" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join('');
          document.getElementById('list').insertBefore(span, null);
        };
      })(f);

      // Read in the image file as a data URL.
      reader.readAsDataURL(f);
    }
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Slicing a file

File() permette di leggere range di byte:

var blob = file.slice(startingByte, endindByte);
reader.readAsBinaryString(blob);

Esempio: notare che si usa onloadend e si controlla evt.target.readyState

<style>
  #byte_content {
    margin: 5px 0;
    max-height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
  }
  #byte_range { margin-top: 5px; }
</style>

<input type="file" id="files" name="file" /> Read bytes: 
<span class="readBytesButtons">
  <button data-startbyte="0" data-endbyte="4">1-5</button>
  <button data-startbyte="5" data-endbyte="14">6-15</button>
  <button data-startbyte="6" data-endbyte="7">7-8</button>
  <button>entire file</button>
</span>

<div id="byte_range"></div>
<div id="byte_content"></div>

<script>
  function readBlob(opt_startByte, opt_stopByte) {

    var files = document.getElementById('files').files;
    if (!files.length) {
      alert('Please select a file!');
      return;
    }

    var file = files[0];
    var start = parseInt(opt_startByte) || 0;
    var stop = parseInt(opt_stopByte) || file.size - 1;

    var reader = new FileReader();

    // If we use onloadend, we need to check the readyState.
    reader.onloadend = function(evt) {
      if (evt.target.readyState == FileReader.DONE) { // DONE == 2
        document.getElementById('byte_content').textContent = evt.target.result;
        document.getElementById('byte_range').textContent = 
            ['Read bytes: ', start + 1, ' - ', stop + 1,
             ' of ', file.size, ' byte file'].join('');
      }
    };

    var blob = file.slice(start, stop + 1);
    reader.readAsBinaryString(blob);
  }
  
  document.querySelector('.readBytesButtons').addEventListener('click', function(evt) {
    if (evt.target.tagName.toLowerCase() == 'button') {
      var startByte = evt.target.getAttribute('data-startbyte');
      var endByte = evt.target.getAttribute('data-endbyte');
      readBlob(startByte, endByte);
    }
  }, false);
</script>

Monitorare il processo di lettura

Si usano gli eventi onloadstart e onprogress.
Esempio: barra di progressione

<style>
  #progress_bar {
    margin: 10px 0;
    padding: 3px;
    border: 1px solid #000;
    font-size: 14px;
    clear: both;
    opacity: 0;
    -moz-transition: opacity 1s linear;
    -o-transition: opacity 1s linear;
    -webkit-transition: opacity 1s linear;
  }
  #progress_bar.loading {
    opacity: 1.0;
  }
  #progress_bar .percent {
    background-color: #99ccff;
    height: auto;
    width: 0;
  }
</style>

<input type="file" id="files" name="file" />
<button onclick="abortRead();">Cancel read</button>

<div id="progress_bar">
<div class="percent">0%</div>
</div>

<script>
  var reader;
  var progress = document.querySelector('.percent');

  function abortRead() {
    reader.abort();
  }

  function errorHandler(evt) {
    switch(evt.target.error.code) {
      case evt.target.error.NOT_FOUND_ERR:
        alert('File Not Found!');
        break;
      case evt.target.error.NOT_READABLE_ERR:
        alert('File is not readable');
        break;
      case evt.target.error.ABORT_ERR:
        break; // noop
      default:
        alert('An error occurred reading this file.');
    };
  }

  function updateProgress(evt) {
    // evt is an ProgressEvent.
    if (evt.lengthComputable) {
      var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
      // Increase the progress bar length.
      if (percentLoaded < 100) {
        progress.style.width = percentLoaded + '%';
        progress.textContent = percentLoaded + '%';
      }
    }
  }

  function handleFileSelect(evt) {
    // Reset progress indicator on new file selection.
    progress.style.width = '0%';
    progress.textContent = '0%';

    reader = new FileReader();
    reader.onerror = errorHandler;
    reader.onprogress = updateProgress;
    reader.onabort = function(e) {
      alert('File read cancelled');
    };
    reader.onloadstart = function(e) {
      document.getElementById('progress_bar').className = 'loading';
    };
    reader.onload = function(e) {
      // Ensure that the progress bar displays 100% at the end.
      progress.style.width = '100%';
      progress.textContent = '100%';
      setTimeout("document.getElementById('progress_bar').className='';", 2000);
    }

    // Read in the image file as a binary string.
    reader.readAsBinaryString(evt.target.files[0]);
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Oggetti JS e HTML

JavaScript può essere usato per “programmare” degli oggetti che rendono “attivo” l’ HTML.

Alcuni esempi tratti da: http://stackoverflow.com/questions/3582185/handle-html-code-block-as-an-object

<div id="book1">
<div id="price">$30</div>    
...
</div>
// oggetti creati con il metodo del costruttore
// si può usare anche Object.create
var Book = function(name) {
   this.name = name;
}

Book.prototype = {
   setPrice : function(price) {
      this.price = price;
   },
   update : function() {
      pricediv = document.getElementById(this.name)
      pricediv.innerHTML = '$'+price;
   }
}

var book = new Book('book1')
book.setPrice(50);
book.update();

MicroTemplate e Cascade

var Book = function(){
    var price    =   args.price  || 0,
        color    =   args.color  || 'red',
        height   =   args.height || '200px',
        width    =   args.width  || '600px',
        template =   "<div style='background-color: COLOR; width=WIDTH; height: HEIGHT;'><span>$PRICE</span><br/></div>";

    return {
        setPrice:  function(np){
             price = np;
             return this;
        },
        setColor:  function(nc){
             color = nc;
             return this;
        },
        setHeight: function(nh){
             height = nh;
             return this;
        },
        render:    function(){
             template = template.replace(/COLOR/, color);
             template = template.replace(/PRICE/, price);
             // etc
             // use jQuery or native javascript to form and append your html
             $(template).appendTo(document.body);
        }
    };
};
// Uso:
var book = Book({
    price:  30,
    color:  'blue'
});

book.render();
// modifica dei valori:
book.setPrice(140).setColor('yellow').setHeight('500').render();

Estendere il DOM

http://robskelly.com/2011/11/subclassing-dom-elements-in-javascript/

Estendendo li oggetti del DOM si possono creare nuovi oggetti con più funzionalità.
Nel link si fa questo esempio: si estende HTMLImageElement seguendo il Factory Pattern.
Le variabili locali verranno aggiunte all’ elemento DOM con le closures, mentre con Object.defineProperty si definiranno le proprietà pubbliche.
Nell’ esempio si definisce una sotto-classe di HTMLImageElement con un setter che riceve un oggetto Image customizzato per contentere le proprietà src e title.
Quando Image vieneimpostato in HTMLImageElement, verranno automaticamente impostati source e alt.

L’ oggetto Image:

/**
* Represents an Image.
* @param src The source URL of the image file.
* @param title The title of the image.
*/
function Image(src, title){
    this.title = title;
    this.src = src;
}
Image.prototype = new Object();

La sotto-classe:

/**
* Creates a "subclass" of HTMLImageElement.
*/
function newImageElement(){
 
    // Create a regular img element.
    var img = document.createElement("img");
 
    // A variable to hold the Image instance.
    var _image = null;
 
    // Create a property. The get and set closures will keep
    // the _image variable in scope. The setter sets the properties
    // on the img element.
    Object.defineProperty(img, "image", {
        get : function(){
        return _image;
    },
    set : function(value){
        _image = value;
        this.src = value.src;
        this.alt = value.title;
    }
    });
 
    // Return the modified img element.
    return img;
}

Per usare il factory method:

// Create an img element.
var img = newImageElement();
 
// Create the image object.
var image = new Image("http://farm6.static.flickr.com/5187/5774436039_28352f0d8f.jpg", "A cyclist.");
 
// Set the image on the img.
img.image = image;
 
// Append the img on the document's body.
document.body.appendChild(img);

Appunti AJAX

Gestire eventi su HTML dinamico

Non so quale possa essere il modo migliore per gestire gli eventi sull’ HTML ricevuto tramite XMLHttpRequest, nel senso che ci sono tanti modi diversi per farlo, ma non trovo la soluzione migliore.
Forse jQuery permette una soluzione “semplice”, ma io vorrei usare solo JavaScript con PrototypeJS.

Primo caso

Ricevo HTML con una richiesta gestita da Ajax.Updater, e la risposta contiene javascript inline:

new Ajax.Updater('div_id', '/path/to/get_smtg.php', {
// notare evalScripts
  parameters: { param1: 'value1', evalScripts: true) }
});

ogni blocco <script> viene “valutato” da javascript.
Questo può essere un buon metodo, ma ha 2 difetti:

  • non c’è separazione del codice, dato che javascript e PHP risultano mescolati
  • ogni volta che viene chiamato Ajax.Updater deve essere valutato ogni script ricevuto (ad esempio gli event listeners vengono rimossi e ricollegati)

Un pregio è quello di centralizzare la logica che gestisce l’ HTML ricevuto dinamicamente.

Secondo caso

Per ovviare ai precedenti difetti si può pensare alla delegazione degli eventi:
invece di aggiungere i listener all’ HTML ricevuto, si pone un listener sul suo contenitore.
Non sono pienamente convinto di queso metodo: non mi piace che ci siano degli script attivi su un contenuto che non esiste e che potrebbe non essere raggiunto con AJAX.
Si può mitigare questo comportamento registrando i listener con la classe Event.Handler di prototypeJS:
il costruttore istanzia un oggetto “handler” che però non osserva gli eventi finchè non viene chiamato il metodo start

var handler = new Event.Handler(element, eventName[, selector], callback);
handler.start();
...
handler.stop();

Terzo caso

Dovrei vedere come realizzare un widget in cui fondere HTML e JavaScript.
http://www.amazon.com/Developing-Widget-HTML-JSON-AJAX/dp/1450502288
http://developingwebwidgets.blogspot.it

Restituire HTML e JavaScript da PHP

Per restituire JS inline al codice HTML bisogna costruire la stringa della risposta.

heredoc e nowdoc

PHP permette di assegnare testo multilinea ad una variabile stringa tramite gli heredoc e, dalla versione 5.3.0, anche tramite i nowdoc:
la differenza fra i due è che heredoc subisce il parsing di PHP, mentre nowdoc no.

// heredoc:
$name = 'MyName';
$variable = "text";
echo <<<EOT
My name is "$name". I am printing some $variable.
This should print a capital 'A': \x41
EOT; // notare il punto-e-virgola

Output:

My name is “MyName”. I am printing some text.
Now, I am printing some Bar2.
This should print a capital ‘A’: A

Invece con nowdoc:

// nowdoc:
$name = 'MyName';
$variable = "text";
// notare il single-quote
echo <<<'EOT'
My name is "$name". I am printing some $variable.
This should not print a capital 'A': \x41
EOT;

Output:

My name is “$name”. I am printing some $variable.
This should not print a capital ‘A’: \x41

Includere un file esterno

Il comando include di PHP legge un file e lo riporta nel file attuale.
Ci sono però varie problematiche da tenere presente:

  • una è include_path
  • poi c’è il comportamento del return per assegnare l’ output ad una variabile
  • infine si può usare l’ output buffering per ricevere l’ output di include

Vedere il manuale di include.

Oppure bisogna leggere un file con fopen(), e restituire il suo contenuto.

Javacript get/set e UI

http://www.mitya.co.uk/blog/2012/Feb/ECMAScript-revolution-object-properties-201

ECMAScript 5: a revolution in object properties

Over the coming weeks I’m going to focus on discussing the mini revolution that ECMAScript 5 brought, and the implications in particular for objects and their properties.

ECMA5’s final draft was published at the end of 2009, but it was only really when IE9 launched in early 2011 – and, with it, impressive compatibility for ECMA5 – that it became a genuinely usable prospect. Now in 2012, it is being used more and more as browser vendors support it and its power becomes apparent. (Full ECMA5 compatibility table).

JavaScript has always been a bit of an untyped, unruly free-for-all. ECMAScript 5 remedies that somewhat by giving you much greater control over what, if anything, can happen to object properties once changed – and it’s this I’ll be looking at in this first post.

A new approach to object properties

In fact the whole idea of an object property has changed; it’s no longer a case of it simply being a name-value pairing – you can now dictate all sorts of configuration and behaviour for the property. The new configurations available to each property are:

  • value – the property’s value (obviously)
  • writable – whether the property can be overwritten
  • configurable – whether the property can be deleted or have its configuration properties changed
  • enumerable – whether it will show up in a for-in loop
  • get – a function to fire when the property’s value is read
  • set – a function to fire when the property’s value is set

Collectively, these new configuration properties are called a property’s descriptor. What’s vital to understand, though, is that some are incompatible with others.

Two flavours of objects

The extensive MDN article on ECMAScript 5 properties suggests thinking of object properties in two flavours:

  • data descriptors – a property that has a value. In its descriptor you can set value and writable but NOT get or set
  • accessor descriptors – a property described not by a value but by a pair of getter-setter functions. In its descriptor you can set get and set but NOT value or writable.

Note that enumerable and configurable are usable on both types of property. I’m struggling to understand why someone thought the ability to set a value and a setter function, for example, were incompatible desires. If I find out, I’ll let you know.

New methods

To harness this new power, you need to define properties in one of three ways – all stored as new static methods of the base Object object:

  • defineProperty()
  • defineProperties()
  • create()

The first two work identically except the latter allows you to set multiple properties in one go. As for Object.create(), I’ll be covering that separately in a forthcoming post.

Object.defineProperty() is arguably the most important part of this new ECMAScript spec; as John Resig points out in his post on the new features, practically every other new feature relies on this methd.

Object.defineProperty() accepts three arguments:

  • the object you wish to add a property to
  • the name of the property you wish to add
  • a descriptor object to configure the property (see descriptor properties above)

Let’s see it in action.

var obj1 = {};
Object.defineProperty(obj1, 'newProp', {value: 'new value', writable: false});
obj1.newProp = 'changed value';
console.log(obj1.newProp); //new value - no overwritten

See how the overwrite failed? No error or warning is thrown – it simply fails silently. In ECMA5’s new ‘strict mode’, though, it does throw an exception. (Thanks to Michiel van Eerd for pointing this out.)

There we set a data descriptor. Let’s set an accessor descriptor instead.

var obj = {}, newPropVal;
Object.defineProperty(obj, 'newProp', {
    get: function() { return newPropVal; },
    set: function(newVal) { newPropVal = newVal; }
});
obj.newProp = 'new val';
console.log(obj.newProp); //new val

You might be wondering what on earth is going on with that newPropVal variable. I’ll come to that in my next post which will look at getters and setters in detail. Note also how, with our setter, the new value is forwarded to it as its only argument, as you’d expect.

The fact that these properties can be set only via these methods means you cannot create them by hand or in JSON files. So you can’t do:

var obj = {prop: {value: 'some val', writable: false}}; //etc
obj.prop = 'overwritten'; //works; it's not write-protected

ECMA 5 properties don’t replace old-style ones

An important thing to understand early on is that this new form of ‘uber’ property is not the default. If you define properties in the old way, they will behave like before.

var obj = {prop: 'val'};
obj.prop = 'new val'; //overwritte - no problemo

Reporting back

Note that these new configuration properties are, once set, not available via the API; rather, they are remembered in the ECMAScript engine itself. So you can’t do this (using the example above):

console.log(obj1.newProp.writable); //error; newProp is not an object

Instead, you’ll be needing Object.getOwnPropertyDescriptor. This takes two arguments – the object in question and the property you want to know about. It returns the same descriptor object you set above, so something like:

{value: 'new value', writable: true, configurable: true, enumerable: true}

More to come..

So there you go – a very exciting mini revolution, as I said. This new breed of intelligent object property really is at the heart of arguably the most major shake-up to the language for a long time. Next week I’ll continue this theme – stay posted!

http://www.mitya.co.uk/blog/2012/Mar/JavaScript-getters-and-setters-varying-approaches-204

JavaScript getters and setters: varying approaches

Getters and setters are a means of providing an arm’s-length way of getting or setting certain data, whilst keeping private other data, and are common of most languages. In JavasScript, setters are also a good way of ensuring your UI stays up to date as your data changes, which I’ll show you an example implementation further down.

A new approach to getters and setters

The new approach looks like this, and can be used only on properties created via the new Object.create() and Object.definePropert[y/ies]() methods.

var dog = {}, name;
var name;
Object.defineProperty(dog, 'name', {
    get: function() { return name; },
    set: function(newName) { name = newName; }
});
dog.name = 'Fido';
alert(dog.name); //Fido

You’ll note that this approach requires the help of a ‘tracker variable’ (in our case name) via which the getter/setter reference the property’s value. This is to avoid maximum recursion errors that the following would cause:

...
    get: function() { return this.name; }, //MR error
...

That happens because we set a getter, via which any attempt to read the property is routed. Therefore, having the getter reference this.name is effectively asking the getter to call itself – endlessly. Likewise for a setter, if it tried to assign to this.name.

Since each property needs its own tracker, and you don’t want lots of variables flying around, it’s a good idea to use a closure when declaring several properties.

var dog = {}, props = {name: 'Fido', type: 'spaniel', age: 4};
for (var prop in props)
    (function() {
        var propVal = props[prop];
        Object.defineProperty(dog, prop, {
            get: function() { return propVal; },
            set: function(newVal) { propVal = newVal; }
        });
    })()
alert(dog.name+' is a '+dog.type); //Fido is a spaniel
dog.name = 'Rex';
alert(dog.name+' is age '+dog.age); //Rex is age 4

There, we declare what properties we want on our object, and some start values. The loop sets each property, and tracks its value via a private propVal variable in its closure.

One of the things I like about this new approach is you no longer have to call the getters/setters explicitly (as you did with previous implementations – see below) – they fire simply by talking to the property.

Admittedly this has its proponents and its opponents; those in favour say getters/setters should fire simply by calling/assigning to the property – not calling some special methods to do that. Those against normally point out that someone new to the code might be surprised to find that talking to a property in fact fires a function.

My take is that, as long as this is part of the spec, and your code is well documented, there can be few complaints with using the new implementation.

Other ways of doing getters/setters

In any case, I much prefer them to the implementation we got in JavaScript 1.5.

var dog = {
    type: 'Labrador',
    get foo() { return this.type; },
    set foo(newType) { this.type = newType; }
};
alert(dog.type); //Labrador
dog.foo = 'Rotweiller';
alert(dog.foo); //Rotweiller

I’ve never been in love with this approach, chiefly because you don’t deal directly with the property but with a proxy that represents its getter/setter callbacks – in the above example foo. The new approach does away with this; you call/assign to the property just as you would if there were no getters/setters in play, and the getter/setter callbacks kick in automatically – they are not referenced explicitly.

That said, one good point about this separation of property value from getter/setter is that the getter/setter can safely reference the property via this without the risk of recursion error, as befalls the new approach.

The older way

There’s also the depracated __defineGetter__() and __defineSetter__() technique.

var dog = {
    type: 'Labrador'    
};
dog.__defineGetter__('get', function() { return this.type; });
alert(dog.get); //Labrador

Once again you have to name your setters/getters. By far the most notable point about this approach, though, is you can assign getters/setters after assigning the property – not a super common desire, but useful any time you don’t want to or can’t alter the prototype. The other two implementations don’t allow you to do this, at least without a lot of reworking.

A final point about these latter implementations is that they don’t hijack control of your property like the new implementation does. That is, if a developer ignores them and manipulates the property directly, they can. This is not good news; if you defined getters/setters, you probably want them to run, not be bypassed.

var dog = {
    name: 'Henry',
    set foo(newName) { alert('Hi from the setter!'); this.name = newName; }
};
dog.name = 'Rex'; //setter bypassed; its alert doesn't fire

Setters and a responsive UI

As I mentioned in the intro, another role of getters in JavaScript can be to keep your UI up to date as your data changes. Frameworks like Backbone JS sell themselves heavily on this concept.

As the intro to the Backbone documentation points out, medium-large JavaScript applications can easily get bogged down with jQuery selectors and other means trying to keep your views in-sync with your data.

A getter can help here. Here’s something I cooked up:

Object.UIify = function(obj) {
    for(var property in obj) {
        var orig = obj[property];
        (function() {
            var propVal;
            Object.defineProperty(obj, property, {
                get: function() { return propVal; },
                set: (function(target) {
                    return function(newVal) {
                        propVal = newVal;
                        $(target).text(propVal);
                    };
                })(orig.target)
            });
        })()
        obj[property] = orig.val;
    }
return obj;
};

$(function() {
    var dog = Object.UIify({name: {val: 'Fido', target: '#name'}, type: {val: 'Labrador', target: '#type'}});
    dog.name = 'Bert';
    dog.type = 'Rotweiller';
});

And here’s some example HTML:

<p id='dog'>Hi - my name's <span id='name'></span> and I'm a <span id='type'></span>!</p>

I’ll go into the details of what my method does in a further post. Essentially, though, what’s happening is we pass an object to the UIify() method where each property is a sub-object containing its starting value (val) and a CSS/jQuery selector pointing to to the UI element that should be updated as and when the value changes (target.)

UIify() then returns an object using the new ECMA5 getters/setters. Whenever a property of the object is overwritten, the corresponding UI element denoted by the target we specified is updated. In my case, the targets were simply elements with IDs, but it could of course be more complex targets – it’s just CSS/jQuery selector syntax.

———

So there you have it, three approaches through the ages. Next time up I’ll be looking more at the new Object funcionality in ECMA5.

(p.s. for further reading, be sure to check out the extensive MDN article on working with objects, which talks a lot about getter/setter techniques.)

HTML contenteditable

Già da molto tempo esiste l’ attributo contenteditable per gli elementi HTML, e la compatibilità è abbastanza buona con tutti i browser.

Quando si edita un elemento, al refresh si perdono le modifiche: per memorizzarle è necessario gestire alcuni eventi quali, ad esempio, la pressione del tasto Invio o Canc.

Vedere l’ esempio di Remy Sharp:
http://jsbin.com/owavu3

document.addEventListener('keydown', function (event) {
  var esc = event.which == 27,
      nl = event.which == 13,
      el = event.target,
      input = el.nodeName != 'INPUT' && el.nodeName != 'TEXTAREA',
      data = {};

  if (input) {
    if (esc) {
      // restore state
      document.execCommand('undo');
      el.blur();
    } else if (nl) {
      // save
      data[el.getAttribute('data-name')] = el.innerHTML;

      // we could send an ajax request to update the field
      /*
      $.ajax({
        url: window.location.toString(),
        data: data,
        type: 'post'
      });
      */
      log(JSON.stringify(data));

      el.blur();
      event.preventDefault();
    }
  }
}, true);

function log(s) {
  document.getElementById('debug').innerHTML = 'value changed to: ' + s;
}
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>JS Bin</title>
<style>
[contenteditable]:hover:after {
  content: ' click to edit';
  font-style: italic;
  font-size: 12px;
  font-family: sans-serif;
  color: #ccc;
  .text-stroke(0);
}

[contenteditable]:hover, [contenteditable]:focus {
  background: #FFFFD3;
}

[contenteditable]:focus {
  padding: 5px;
}

[contenteditable]:focus:after {
  content: '';
}
</style>
</head>
<body>
  <p contenteditable data-name="title" id="hello">Hello World</p>
  <div id="debug"></div>
</body>
</html>

AJAX: risposta JSON da PHP

Questa tecnica ha diversi vantaggi:

  • javascript può processare nativamente i dati JSON
  • il lavoro sul server è ridotto

Sul server si esegue una query, e il fetch restituisce un array per ogni recordset (riga), array che può essere numerato o associativo; in ogni caso, basta creare un array con gli array di ogni riga, dopodichè si crea la stringa JSON e la si restituisce allo script chiamante:

<?php
function jsonFromQuery($result) {
    if(mysql_num_rows($result)>0) {
        while($r = mysql_fetch_array($result, MYSQL_ASSOC)) {
            $json[] = $r;
        }
    } else {
        $json = "La tavola è vuota";
    }
    return json_encode($json);
}

$conn = mysql_connect("hostname","username","password");
mysql_select_db("databasename",$conn);
$query = "SELECT * FROM tablename";
$res = mysql_query($query);
$json = jsonFromQuery($res);
header("Content-type: application/json");
print $json;
?>