Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
JavaScript library that synthesizes properties with types, operators and more
JavaScript
branch: master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
docs
test
.gitignore
.jshintrc
.npmignore
.travis.yml
LICENSE
Makefile
README.md
bower.json
package.json
synths.js

README.md

Synths Build Status NPM version

Synths is a JavaScript library that synthesizes properties with type, methods, operators and more. It is inspired by the @synthesize feature in the Objective C language.

Synthesized property

Considering the interface of a person's name, there are several different styles or approaches such as

  1. Not very common: person.get_name() and person.set_name('Tom').
  2. Java convention: person.getName() and person.setName('Tom').
  3. Backbone library model: person.get('name') and person.set('name', 'Tom').
  4. Qt framework: person.name() (getter) and person.setName('Tom').
  5. jQuery-flavor: person.name() and person.name('Tom') for getter and setter, respectively.
  6. JavaScript ES5 accessor property: person.name and person.name = 'Tom' implicitly invoke the getter and the setter, respectively.

These are all doing about the same thing but with different look and feel. Methods in cases 1-4 are generally called accessor (getter) and mutator (setter), while case 6 is the typical language defined property. It is inter

In this library, only case 5 is considered, and is referred to as a synthesized property or shortly a property. Thereby, the normal JavaScript property is referred to as an instance variable.

Why synths

  1. A property is one while a getter and a setter are two. This one implicitly does the same as these two in a more concise way.
  2. IE 8 is still some user's preferred browser, and the ES5 property is not there. Synths fills the gap for developers who wish to use the notion of property for this legacy browser.
  3. Synthesized property provides something more than the ES5 property. You may find something interesting such as the chainable setter var mary = new Person().name('Mary').age(20); for object initialization, smart property mary.husband('name', 'John'), negative array index mary.hobbies(-1), sending a message to the property mary.hobbies('#any', function (hobby) { return hobby === 'eat'; }), and operator mary.age('+=', 1), etc.
  4. Synths provides an operator extension and overloading mechanism for a property. For example, var v0 = new Vector().x(1).y(2), v1 = ..., var line = new Line().from(v0).to(v1) and line.from('+=', v3).

Install

For a NodeJS project, use

npm install synths

For a browser project, use

bower install synths

If the above does not fit for you, use git to clone this project or download the zip file.

Usage

var synthesize = require('synths').synthesize;
// or var synthesize = synths.synthesize;  // if synths is loaded as a browser global.
// or for AMD
// define(['synths', function (synths) {
//     var synthesize = synths.synthesize; ... });

var john = {

    // Basic property
    firstName: synthesize(),
    lastName: synthesize(),
    hobbies: synthesize(),

    // Property with a type
    age: synthesize('integer'),

    // Constant property
    sex: synthesize({ constant: 'male' }),

    // Property with a default value
    // Default value will be lazily initialized when calling the getter.
    // If the value is an object or an array, it will be cloned.
    weight: synthesize({ default: 80 }),

    // Custom property
    // if either get or set is defined, a read-only or a write-only property will be synthesized,
    // in which the value of get or set can be a function or an instance variable name.
    name: synthesize({
        get: function () {
            return this.firstName() + ' ' + this.lastName();
        },
        set: function (n) {
            n = n.split(' ');
            this.firstName(n[0]).lastName(n[1]);
        }
    }),

    // toJSON is synthesized for the JSON.stringify()
    // Notice that toJSON is **not** a property though it is synthesized.
    toJSON: synthesize('toJSON')
};

// Chainable setters
john.name('John Smith').age(25.5).sex('female').hobbies(['sleep', 'eat']);

console.log(john.firstName());          // 'John'
console.log(john.age());                // 25       (converted to an integer)
console.log(john.sex());                // 'male'   (constant)
console.log(john.weight());             // 80       (default)
console.log(john.hobbies(0));           // 'sleep'  (array index)
john.hobbies('#push', 'JavaScript');
console.log(john.hobbies('length'));    // 3
console.log(john.hobbies(-1));          // 'JavaScript'  (negative index)
console.log(john.toJSON());     // { firstName: 'John',
                                //   lastName: 'Smith',
                                //   hobbies: [ 'sleep', 'eat', 'JavaScript' ],
                                //   age: 25,
                                //   sex: 'male',
                                //   weight: 80,
                                //   name: 'John Smith' }

Normally you want to synthesize a property for the object prototype, and the usage is the same. Likely, you will do nothing inside the constructor because the default value is already defined in the property. Chainable setters are recommended for initializing the object because they are more flexible and expressive.

var synths = require('synths'),
    synthesize = synths.synthesize,
    _ = synths._;

function Person () {}
_.extend(Person.prototype, {
    name: synthesize(),
    age: synthesize(),
    mood: synthesize({ default: 'happy' }),
    toJSON: synthesize('toJSON')
});

var mary = new Person().name('Mary').age(18);
console.log(mary.toJSON());         // { name: 'Mary', age: 18, mood: 'happy' }

Syntactic sugar

Metalinguistic abstraction is experimented in this library, such as

  • Smart property

    Synthesized property is smart. When the current value of a property is an object, the getter and setter mechanism changes automatically. For example, executing john.wife(mary) would set john's wife property to be the mary object. Later on, john.wife('name') becomes a getter and john.wife('name', 'Mary') is a setter method. The name of the mary object can be simply a key value pair or a synthesized property.

  • Array index

    The same rule applies to an array property. For example, john.hobbies(0) get the value of property hobbies at index 0, and john.hobbies(0, 'eat') sets john's hobbies at index 0 to be 'eat'. Negative index is supported as well; for example, john.hobbies(-1) gets the last hobby of John. The length is john.hobbies('length').

  • Message or method

    A message can be sent to a property such as john.age('#toString') or the alias john.age('#to string') is the same as john.age().toString(); john.hobbies('#push', 'drink'), john.hobbies('#pop'), etc. All lodash methods are also available, e.g. john.hobbies('#any', function (hobby) { return hobby === 'sleep' }) will return true.

  • Nested properties

    The smart property can be nested. For example, john.wife('hobbies', 1) gets John's wife's second hobby and john.wife('hobbies', 1, 'sleep') set John's wife's second hobby to be 'sleep'. These are the same as john.wife().hobbies(1) and john.wife().hobbies(1, 'sleep'), respectively. A message can be sent as well; e.g. john.wife('hobbies', '#push', 'picnic') is the same as john.wife().hobbies('#push', 'picnic').

  • Binary operator

    The example in the previous secion, this.firstName() + ' ' + this.lastName(), can be written as

this.firstName('+', ' ', this.lastName());

or equivalent to the reverse addition or right-to-left concatenation r+ as follows

this.lastName('r+', ' ', this.firstName());

Assignment operator such as += can be used as well, for example this.firstName('+=', 'Mr.'). Operators can be extended or overloaded (to be described).

Syntax

  • Chainable setter john.name('John').age(25)
  • Getter john.name() returns 'John'
  • Binary operator obj.prop('+', val) is equivalent to obj.prop() + val.
  • Assignment operator obj.prop('+=', val) is equivalent to obj.prop(obj.prop() + val).
  • Method of property obj.prop('#toString'), and aliases: obj.prop('#to string'), obj.prop('#to-string'), or obj.prop('#to_string'), are equivalent to obj.prop().toString().
  • Lodash method obj.prop('#forEach', getName) and aliases obj.prop('#for each', getName), obj.prop('#for-each', getName), or obj.prop('#for_each', getName) are equivalent to _.forEach(obj.prop(), getName).
  • Property's property obj.prop('subProp') and aliases: obj.prop('sub prop'), obj.prop('sub-prop')obj.prop('sub_prop'), are equivalent to obj.prop().subProp(). Setter obj.prop('subProp', 123) is equivalent to obj.prop().subProp(123).
  • Property's key, obj.prop('key') is equivalent to obj.prop().key. Setter obj.prop('key', val) is equivalent to obj.prop().key = val.
  • Property's index obj.prop(1), equivalent to obj.prop()[1]. obj.prop(1, 'foo') is equivalent to obj.prop()[1] = 'foo'. Negative index: obj.prop(-1) is equivalent to obj.prop(obj.prop().length -1).

There are more features not mentioned here yet. You may want to find more details in the test documentation (either docs/test.md or http://malcomwu.github.io/synths/)

Dependency

The only dependency of synths is lodash; however, you can simply replace it by underscore if you wish.

Pitfall

Because # prefix is reserved for method invocation, it will fail if a string starting with a # character is intended to be set. This behavior might cause unexpected problems, and should be corrected in the future.

Presumably, you will not be able to send a message to a property which is a string in the future. For other types, you can safely send a message without any problem.

Currently you can fix it, if necessary, by making a custom property and escape the starting # sign such as

synthesize({
    get: function () { ...; },
    set: function (x) {
        x = x.charAt(0) === '#' ? '#' + x : x;
        this.someProp(x);
    }
});

Future work to be considered

  • Improve documentation
  • Remove the pitfall described above
  • Improve type system
  • Add validation feature
  • Observer

You are very welcome to help for improving this library.

Something went wrong with that request. Please try again.