Skip to content

Instantly share code, notes, and snippets.

@anthonyshort
Last active August 29, 2015 14:05
Show Gist options
  • Save anthonyshort/2dbf56c398f320a4db61 to your computer and use it in GitHub Desktop.
Save anthonyshort/2dbf56c398f320a4db61 to your computer and use it in GitHub Desktop.
ripple 0.6
var ripple = require('ripple');
var binder = require('binder');
var template = require('./template.html');
// Data-binding engine
var bindings = binder(template)
.use(someDirective)
.use(someFiltersAndShit);
var View = ripple()
.engine(bindings)
.attr('firstName')
.attr('lastName');
module.exports = View.extend({
initialize: function(){
console.log('initializing a view!');
},
onClick: function(event){
console.log('clicked!');
}
});
var ripple = require('ripple');
var binder = require('binder');
var template = require('./template.html');
// Data-binding engine
var bindings = binder(template)
.use(someDirective)
.use(someFiltersAndShit);
var View = ripple({
initialize: function(){
console.log('initializing a view!');
},
onClick: function(event){
console.log('clicked!');
}
});
View
.engine(bindings)
.attr('firstName')
.attr('lastName');
module.exports = View;
import ripple from 'ripple';
import binder from 'binder';
import template from './template';
var bindings = binder()
.use(someDirective)
.use(someFiltersAndShit);
var Base = ripple()
.engine(bindings(template))
.attr('firstName')
.attr('lastName');
exports class View extends Base {
constructor() {
super();
console.log('initializing a view!');
},
onClick(event) {
console.log('clicked!');
}
}
var ripple = require('ripple');
var virtual = require('virtual');
var template = require('./template');
var View = ripple({
initialize: function(){
console.log('initializing a view!');
},
onClick: function(event){
console.log('clicked!');
}
});
View
.engine(virtual(template))
.attr('firstName')
.attr('lastName');
module.exports = View;
@anthonyshort
Copy link
Author

One thing I want to do, as Ian suggested a while ago, is remove the use of this in as many places as possible. It either needs to be used everywhere to refer to the view instance, or never used.

@anthonyshort
Copy link
Author

I'm also looking at moving away completely from using the prototype of the view, or even passing in a view/class. I'm not so sure it's needed. But we could still do it.

class ViewThing {
  constructor() {} // Is where initializing is done
  submit() {}
}

var View = ripple(ViewThing)
  .mounted(function(view){ view.startInterval() })
  .engine(bindings(template))
  .attr('email')
  .action('submit') // Registers the submit method above as an action

@anthonyshort
Copy link
Author

The confusing part about the above is that when you create a new View() you won't actually be using ViewThing at all, which is confusing. So you won't have access to things you think you could normally.

@ianstormtaylor
Copy link

Nice, random more thoughts:

Engine

What would the two engine definitions look like there? Reason I ask is because I think the engine is so core to Ripple that it should be even more streamlined into a use case that is ideally:

ripple(template);

And the engine is assumed. Basically by that I mean that if the virtual DOM engine expects a distinguishably different type of object than the template binding one, we could just check what is passed in to determine the engine automatically:

engine = template == string, element, etc
  ? binding
  : virtual;

So that the end user never even needs to care ideally. Or if that's not possible, have it be a super simple option to just toggle:

ripple(template, { engine: 'binding' })
ripple(template) // defaults to 'virtual'

Or similar. Basically when building a new view, I don't really want to be forced to make the engine pieces verbose all the time for simple stuff if possible. If the bindings engine is .use()d then it can mixin in the other methods it exposes into the DSL potentially instead of need to make a separate object.

Actions vs. Helpers vs. Lifecycle

Those types makes a ton of sense to be able to distinguish these different pieces, hadn't thought of this before.

That said, even with the different types of events, I still really like the ability to put things on the .prototype as the way to define pieces, because having them on the DSL starts to get too magicky for me to follow what's going on with them. Instead, what if we just special-cased items in the prototype:

function View() {}

// Lifecycle events are automatically bound for us if they
// exist on the view. This is the only magic part.
View.prototype.onInitialize = function(){};
View.prototype.onMounted = function(){};
View.prototype.onUnmounted = function(){};

// Actions are just regular methods that the user defines
// to do whatever they want.
View.prototype.submit = function(){};
View.prototype.expand = function(){};

// Helpers are also just regular methods that the user 
// defines to do whatever they want.
View.prototype.normalize = function(){};
View.prototype.format = function(){};

// Event handlers are also just regular methods, but 
// since we have the Lifecycle methods, they get to nicely
// follow the same naming pattern.
View.prototype.onSubmit = function(){};
View.prototype.onChange = function(){};

And then I think keeping the {{ this.method(thing) }} in the helpers is still nicest if it means that we have no real magic layer the user has to understand when jumping in there. And it makes it really clear from just reading the template how they'd add another helper if they needed to.

Events

Using .prototype for all of the methods up there is nice if we have to find to a single way to do all 4 types. The only thing that might be better, and more semantically correct, is if instead of having just "methods" we also kept the concept of "events", but also used it for submissions, clicks, etc. too.

What that might look like is that instead of having a template do something like this (syntax might not be quite right but you get the idea):

<button on-click="{{ this.onClick }}">

What if the recommended way to do this was:

<button emit-on="click">

Pseudocode, basically trying to figure out what the template API would look like to let us make the Javascript events API look like this:

View.on('submit', function(view, event) {
  // ...
});

Which could then nicely make all of our event-binding use cases, both the lifecycle and the DOM events, look similar:

View.on('initialize', function(view, attributes){});
View.on('mount', function(view, parent){});
View.on('unmount', function(view){});

View.on('submit', function(view, event){});
View.on('click', function(view, event){});

And then with the actions and helpers as prototype methods you get the total nice picture with little magic:

View.on('initialize', function(view, attributes){});
View.on('mount', function(view, parent){});
View.on('unmount', function(view){});
View.on('submit', function(view, event){});
View.on('click', function(view, event){});

View.prototype.send = function(){};
View.prototype.expand = function(){};
View.prototype.format = function(){};
View.prototype.normalize = function(){};

So you basically end up with just two things: "methods" and "events".


What do you think of all that?

@tj
Copy link

tj commented Aug 15, 2014

I'm pretty out of the loop with state-of-the-art client-side views now haha, not sure if react has anything in interesting to draw from, looks similar to what we have always been trying to do with ripple/reactive but with the gross inline e4x-ish syntax.

I'd be +1 for prototype stuff, especially since ES6 classes (as retarded as they are) is already a little DSL, and +1 on regular .on() for events. Not sure I'd have events on the constructor, maybe both would be helpful, I think that's what we did at cloudup but I can't remember too well now.

Are web component templates somewhat usable now? Might be worth going that direction to future-proof a bit

@anthonyshort
Copy link
Author

Ok, maybe constructor is the way to go then:

import ripple from "ripplejs/ripple";
import binding from "ripplejs/binding-engine";
export ViewModel;

// The class for the template handles things like returning the 
// template string/function, hooking up DOM functionality on mount/destroy,
// the handlers for user interaction with the DOM.
class View {
  constructor(attrs) {
    // do initialisation stuff here
    this.name = attrs.name || 'default name';
  }
  render() {
    return '<div></div>';
  }
  onSubmit(event) {
    event.preventDefault();
    this.name = 'foo';
    this.submit();
  }
  onMount() {
    // will automatically get added as a 'mounted' callback
  }
  onDestroy() {
    // will automatically get called on 'destroyed' event
  }
}

// Create View wrapper. Setting the engine type here automatically
// uses the plugin for the engine type which adds methods to the view.
var ViewModel = ripple(View)
  .engine(binding); // binding, string, or virtual

// Define attributes that define the state of the view
// These work the same as models so plugins can get to the
// options for each attribute to add extra functionality.
ViewModel
  .attr('name', { required: true, type: 'string' })
  .attr('email', { required: true, type: 'email' })

// Add plugins that can add directives, helpers, etc.
ViewModel
  .use(emailTypePlugin)
  .use(someDirectives)

// Lifecycle events of the view. Whenever the view is created,
// destroyed, or added and removed from the DOM an event is fired.
ViewModel
  .on('create', function(view, attributes){});
  .on('mount', function(view, parent){});
  .on('unmount', function(view){});

This works a little better since we're really dealing with a template and not the view. Separating those two seems to get rid of the confusion a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment