emvy
A tiny (<15kb minified) JavaScript MVVM framework that doesn't make a mess.
Introduction
emvy is a framework for creating web-apps that tries to strike a balance between flexibility and structure, aiming to speed up development while still being clear, avoiding "magic" and retaining control. Emvy's main goals are as follows:
- Don't make a mess: Not in your objects, not in your DOM. emvy makes heavy use of closures to store its internal state, rather than sprinkling it everywhere.
- Minimize magic, but still be concise and elegant. emvy aims to make code execution reasonably transparent and easy to follow, even to someone unfamiliar with emvy's structure and concepts.
- Don't decide your stack for you: emvy doesn't care what templating engine you use. emvy pays attention to your data attributes so it can seamlessly swap in new data, but no further. emvy doesn't care if you use jQuery, Zepto, underscore, lodash, whatever. Using a few other utilities is probably a good idea, but emvy has no dependencies.
- emvy has no default store implementation. Thats right, you'll have to make your own. emvy provides a trivial API to interact with, and a reference localstorage implementation that uses depot.js
emvy does however, encourage a few design ideas:
- MVVM: Standing for Model, View, ViewModel, MVVM is similar to MVC but uses data binding to connect models with views. emvy's ViewModel (and ViewCollection) are very thin, merely ferrying events around.
- Hierarchical views. emvy helps build hierarchical view systems, and then passes events up and down the chain easily. emvy's view system is quite similar to the DOM it represents, but abstracts it into data.
- Composition over inheritance: emvy's design is focused around components: functions that extend objects with further functionality. emvy suggests that recurring ideas be expressed in this way in your applications as well. This helps encapsulate functionality away from data.
- Thin routing - The router isn't at the top in emvy's design. Its just another source of events.
Download
Maybe someday soon. For now grab dev builds off GitHub
Example
But does emvy have the obligatory todos example? Yes, yes it does!
It's also available in JavaScript:
Classes
Object
emvy's core object class, from which all other classes inherit, doesn't do much. It merely provides the basis for emvys composition structure, and an extend method for non coffeescripters.
(Object|object).is(component)
is is used to compose objects, adding in your own components or just the ones emvy provides. component can either be a straight component (see components section) or a string referencing one of emvy's components, eg this.is("Stated") will add state machine behavior to an object. In this case additional arguments are passed to the component function. is exists both of the Object class itself for composing static methods, and on all instances.
Object.extend(childProps,childStatics)
Extend is used to implement classical inheritance if you don't use coffeescript. childProps are mixed in to the new classes prototype, childStatics are mixed in directly to the child class. if childProps.constructor is present, it will be used as the constructor.
When using extend, super is automatically called first thing. You don't need to call super in your JS.
object.mixin(obj,ignore)
Directly mixin the property of an object to this one. Kinda like components, but less cool. Ignore lets you specify an array of keys you don't want to mixin.
Model
Models are centralized data stores, which can be persisted to a backend. a Model represents not just a class for making models, but a top level store for all models of that type.
Model.init(func)
Initializes the Model, adding the Evented component and doing a load of other setup. optional func is called after init is done, useful for further setting up the Model.
After extending model, say to a class Todos, Todos.init() must be called immediately afterwards.
Model(data)
Constructor function that makes new model instances from given objects of data. Models are initialized with Evented, Computing, and Attributed components, and contain all the methods of those components. Models are attached initially to their constructor, so all model events appear on the constructor under the "model" prefix.
Model.all()
Returns an array containing all of the model instances created from this Model.
Model.raw()
Returns an array containing all of the model instances data created from this Model. Due to the way data is stored in emvy, this isn't any additional work, so raw() is a good choice provided you don't modify any model data.
Model.reset()
Resets the Model, removing all data and instances.
Model.remove(model)
Deletes a single model. Queues up a delete in the persist queue.
Model.store(newStore)
Sets/gets the store of this model. Calling store() with no args returns the current one, otherwise the store is changed, and a fetch() is done using the new store.
See the reference localStore implementation for how to roll a store.
Model.fetch(cb)
Populates this model with models from the store. Calls reset with the new models, and finally runs cb with an array of the new models.
Model.persist(cb)
Persists changes in all models to the store. All changes are queued up as they are made, (this could allow for incremental rollbacks in the future). persist pushes queued changes to the store, first compacting the queue to a minimal number of store calls.
Model.find(id)
Find and return the model with the given id.
Model.mask(name,func)
Makes a new model mask. Masks function as an event filter, allowing you to receive a subset of a models events. When a model emits an event, the model is passed to func. If func returns true, then the mask also emits that event. Otherwise, the event is suppressed.
Calling Model.mask(name) retrieved a previously defined mask for use.
View
Views are thin event wrappers over the Element component. Views are initialized with the Evented, Element, and Computing components. Views in emvy function as managers for dom events, and proxies for setting and getting dom data. Views have no additional functionality, all view methods can be found in the initial components.
ViewModel
ViewModels serve as a simple connection point for views and models (duh!). View Models are initialized with the Evented and Computing components. ViewModels are really just a convenience class, theres nothing here that couldn't be achieved by manually attaching views and models together. Viewmodels are initialized with the Evented and Computing components
ViewModel(options)
Create a new viewModel. options can contain values for view and model, and this will be used.
viewModel.model(newmodel)
This sets the viewmodel's model. The old model is detached, and the new is attached in its place. If no new model is provided, the current model is returned.
viewModel.view(newview)
Basically the above but with the view.
ViewCollection
ViewCollections serve to manage a load of views for a Model. Responding to events on the model, they create and destroy ViewModel instances and the respective views. ViewCollections are initialized with the Evented component.
ViewCollection(options)
Create a new viewCollection. Options can contain values for parent, outlet, and view. Currently changing these on an viewCollection thats already been used is a really bad idea, its best to just set them in the options and be done with it.
viewCollection.model(newmodel)
This sets the viewColletions's Model. The old Model is detached, and the new is attached in its place. If no new Model is provided, the current model is returned.
viewCollection.parent
Specifies the element into which created views should be inserted.
viewCollection.outlet
Specifies the named outlet within the parted into which created views should be inserted.
viewCollection.view
Specifies the view that should be used with the Model's models.
Router
emvy's router is currently a kind of stand-in for a future router closer to my ideals, once I find out what they are. Nonetheless, it functions, and is pretty versatile. The router is a top level singleton, its not a class. (yep, I know this is the classes section but it would feel lonely on its own!) The router emits events based on the path as soon as it is started by calling emvy.Router().
The events it emits are pretty simple. For each segment of the path, it will emit the segment, with all previous segments as a prefix, and all further segments as arguments. For example, given the path /user/michael/todos/1 the router will emit 4 events as follows:
"user", (michael,todos,1)
"user.michael", (todos,1)
"user.michael.todos", (1)
"user.michael.todos.1" ()
Router.navigate.to(url)
Pushes the url into the history, them emits events as above.
Router.navigate.up()
Pops the last part segment off the path, then calls to() on the new path.
Router.navigate.down(to)
Pushes the given segment onto the path, then calls to() on the new path.
Router.navigate.across(to)
Swaps out the last segment of the path with the given one, then calls to() on the new path.
Components
Evented(tag)
Evented is emvy's event emitter. Pretty much everything uses it.
emvy's event system is bubbling, but what does that even mean in this case?
Every event emitter has an upstream, a downstream, and an optional tag. For every event triggered, the event is applied locally, and provided no local callback returns true, (which stops propagation), the event is then then prepended with the tag, showing its origin, and then send first downstream, and provided nothing there returns true, upstream as well. This lets you control even propagation fairly easily. The tag is supplied when passing a call of Evented to the is function
When applied, Evented provides the following methods:
object.attach(something,oneway)
Sets up a connection between two event emitters. The optional argument oneway configures whether the attachment is bidirectional, default is yes. object will have something in its downstream after the attach, and something will have object upstream.
object.detach(something,oneway)
Removes connections created with attach. oneway dictates if both directions should be detached, defaults to yes.
object.on(action, callback)
Registers a callback for an action. Actions are usually in the form type:period.separated.path though type is not required. if only a callback is provided, that callback will be run on all actions, with the action as its first parameter. Multiple actions can be provided, space separated.
object.once(action, callback)
Like on but callback will only run once.
object.trigger(action, args...)
Triggers an action, running all callbacks associated with it and passing it downstream and upstream. All further arguments are passed to callbacks. Multiple actions can be provided, space separated.
object.off(action, callback)
Remove a callback on an action. If no callback is supplied, all are removed. If no action is supplied, all callbacks on all actions are removed. Multiple actions can be provided, space separated.
Attributed(attributes)
This component powers models, and anything else whose data changes should me monitored. an object can be supplied with the call to Attributed(), providing initial data. In addition, this object also becomes the store for all attributes. This means the actual data can be stored in an object external to the model.
When applied, Attributed provides the following methods:
object.get(key)
Returns the value of key
object.set(key,val)
Sets the value of key to val, first calling validate(key,val) if provided, and not doing anything if validate returns false. Triggers events of type "change" eg "change:text" with val as an argument, and also the generic event "change" with key and val as arguments.
object.all()
returns all the data stored.
Element(tag,html)
Element is the core component that powers a view. Element takes input html, builds a dom node from it, scans the dom node for attributes it understands, then builds cached data structures for fast access to and modification of the data. element also sets up events on the element and listens to them, executing the local methods you specify.
When you add the element component, you can supply an optional tag (defaults to div) and the html the element starts with.
First, lets look at all the data attributes Element recognizes and how to use them.
data-bind
The data-bind attribute lets you specify the property (or computed), whose value you want to be the innerHtml of this element. Unless the element is an input, a textarea, or a select element, in which case data-bind sets the value. eg, an element with the attribute data-bind="todo" would apply apply any incoming events of type change:model.todo to that element
data-outlet
This attribute lets you create named outlets, where other views can insert elements. data-outlet="todos" will create an outlet named todos. See insert and insertInto below for more details on outlets.
data-class
This lets you set up bindings for class names. In its most simple usage, data-class="myClass" would bind the value of myClass on the model or a computed as a class on the element. This isn't normally useful, so in addition emvy supports a ternary syntax for describing toggleable classes depending on boolean attributes. data-class="done?strike" means the element will have the class strike when done is true, and the class will be removed if its false. class="done?strike:red" would mean the class would be replaced with red when done is false.
data-attr
This lets you set up bindings to attribute values. It works nearly identically to class bindings as described above, except the name of the attribute is specified at the start, separated with a |. eg, data-attr="src|img". The ternary syntax described above is fully supported here as well.
data-link
This functions as a helper for calling router navigate actions. The action is provided as the first option, followed by the url. eg data-link="to /test"
data-(click|dblclick|keypress|keydown|keyup|enter)
Binds to all the common input events. (plus one more, enter, for getting enter key presses) These all work in exactly the same way. A function is specified, and if the view has the function, it is executed when the event takes place. The last argument to the function is the actual event, and all other arguments are any additional, space separated values given in the attribute. eg, data-click="myFunc hi there!" calls myFunc("hi", "there!",e)
Element provides the following methods when it is applied:
object.set(key, val)
Sets the value of any data-bindings on key.
object.get(key)
Sort of gets the value of data bindings on key. In reality it gets the first value it finds, and hopes they're all the same (they should be). Also tries to grab attributes bound with data-attr if relevant, but will never grab class values bound with data-class.
object.html(newhtml)
Replaces the elements html with newhtml, rebuilding all caches, and triggering a reset event that should get data applied to it again. Calling this method without newhtml returns the current html
object.insertInto(view,outlet)
Inserts this element into a named outlet on another view, or if view is a string, uses document.querySelector and appendChild to insert it into a dom node described by the string.
object.insert(view,outlet)
Inserts another view into a named outlet on this element.
object.remove()
Removes this element from the dom
object.clean(outlet)
Removes all elements from the given outlet. If no outlet is provided, removes all elements from all outlets.
Hiding(key,initialVal,set,get)
This component lets you specify attributes to be "hidden" behind a setter/getter. it is used internally quite a bit, for example in the ViewModel for running changes to the model and the view through a setter first.
Hiding is called supplying a key, the name of the resulting function on the object, an initial value, which is first run through the setter, a setter function, and a getter function. It create a function in the form: object.key(newval) where newval is not supplied to get the current val, which you should recognize from the rest of emvy.
If newval is supplied, the setter function is called like so set(currentValue,newValue) and its return value is taken to be the final new value. Otherwise the getter is called with the current value, and the value returned by the getter is returned as the final current value.
Computing()
Computing adds the computed function to the object, which allows the creation of computed values that depend on other values.
object.computed(name, func, deps)
Creates a new computed value. name is the name of the value, when its changes are broadcast in events. func is the function called to compute the value, and deps is an array of properties whose changes this computed depends on. deps can also contain Models, in which case all changes on the model will cause recalculation of the computed. When a change in any of its dependencies occurs, func is called, and the value returned from func is emitted.
Stateful(states)
Stateful is emvys implementation of a Finite State Machine. States can be initially defined by an object states that maps keys of state names to objects of properties that should be changed for that state. Setter/getter properties, such as those created by Hiding, are taken into account. Stateful objects emit events on state change, one event simply showing the new state state:newstate one showing the transition that occurred state:initial->newstate and one just showing the exit of the old state state:initial->
The initial state of the object is stored in the special state initial.
object.transition(name)
Transition to state name
object.addState(name,state)
Add the state described by state as name