24

Lets imagine function like this:

function foo(x) {
    x += '+';
    return x;
}

Usage of it would be like:

var x, y;
x = 'Notepad';
y = foo(x);
console.log(y); // Prints 'Notepad+'.

I'm looking for a way to create function that's chainable with other functions.

Imagine usage:

var x, y;
x = 'Notepad';
y = x.foo().foo().toUpperCase(); // Prints 'NOTEPAD++'.
console.log(y);

How would I do this?

Phrogz
  • 296,393
  • 112
  • 651
  • 745
daGrevis
  • 21,014
  • 37
  • 100
  • 139

3 Answers3

26

Sure, the trick is to return the object once you're done modifying it:

String.prototype.foo = function() {
    return this + "+";
}

var str = "Notepad";
console.log(str.foo().foo().toUpperCase());

http://jsfiddle.net/Xeon06/vyFek/

To make the method available on String, I'm modifying it's prototype. Be careful not to do this on Object though, as it can cause problems when enumerating over their properties.

Alex Turpin
  • 46,743
  • 23
  • 113
  • 145
  • 5
    it's a good idea to at least check for a property on the native types before adding it, i.e. `if ( !('foo' in String.prototype) ) {String.prototype.foo = function() {...} }` – keeganwatkins Oct 11 '11 at 18:03
  • 1
    If you want to extend an object without breaking the enumeration, use the (semi-modern) [`Object.defineProperty`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty): `Object.defineProperty( String.prototype, {value:function(){ return this+"+"; } } )`. By default the `enumerable` flag is set to `false`. – Phrogz Oct 11 '11 at 18:04
  • @keeganwatkins yes it is :). I assume the OP was only asking about strings as an example, so I kept the warnings to a minimum, but that's a good point. – Alex Turpin Oct 11 '11 at 18:04
  • Good solution, mine was too generic – Andrey Oct 11 '11 at 18:04
  • Hmm... I have line like `this = this.replace(diacritics[i].letters, diacritics[i].base);`. It gives me 'Invalid assignment left-hand side.' in NetBeans. – daGrevis Oct 11 '11 at 18:23
  • [This](http://pastie.org/2678472) is how my code looks. Error is thrown on line #16. – daGrevis Oct 11 '11 at 18:36
  • 1
    You can't assign `this` in a string. Return the result instead. If you need to do multiple operations, store it in a temporary variable and return that instead. For example, `var str = this; str += "foo"; return str;` – Alex Turpin Oct 11 '11 at 18:47
  • Isn't it possible to create a chainable function attached to an object instead of to an String? – Alvaro Oct 28 '14 at 15:29
  • @Alvaro Yes. A string is an object. – Alex Turpin Oct 28 '14 at 16:03
  • @AlexTurpin I mean, an element taken from the DOM using `document.querySelector` – Alvaro Oct 28 '14 at 16:42
  • @Alvaro should be. I would create a separate SO question with your problem – Alex Turpin Oct 29 '14 at 20:17
11

If I remember correctly, you can use "this" as a context of a function (object it belongs to) and return it to make the function chainable. In other words:

var obj = 
{
    f1: function() { ...do something...; return this;},
    f2: function() { ...do something...; return this;}
}

then you can chain the calls like obj.f1().f2()

Keep in mind, you won't be able to achieve what you are expecting by calling obj.f1().toUpperCase() - it will execute f1(), return "this" and will try to call obj.toUpperCase().

Andrey
  • 20,487
  • 26
  • 108
  • 176
1

Here's a way to do it without messing with String.prototype, by returning an object similar to a string, with an additional method foo(). However, there are some downsides to this approach related to it not returning an actual string.

// Returns an object similar to a string, with an additional method foo()
function foo(str) {
  return Object.assign(`${str ?? this}+`, {
    foo
  });
}

var str = "Notepad";
console.log(
  "EXAMPLE - foo(str).foo().toUpperCase():",  
  foo(str).foo().toUpperCase()
);

console.log("---");

console.log("Some issues with this solution:");
console.log("typeof foo(str):", typeof foo(str));
console.log("foo(str).foo():", foo(str).foo());

console.log(
  "You may need to use toString() - foo(str).foo().toString():",
  foo(str).foo().toString()
);
.as-console-wrapper { min-height: 100% }