diff --git a/.gitignore b/.gitignore index 56d3518d640..7a5c1c09b67 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ docs* *.DS_Store .idea bower_components/ -test/pluginified/latest.js \ No newline at end of file +test/pluginified/latest.js +docco \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index d848652d594..3181e533ec2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -376,15 +376,22 @@ module.exports = function (grunt) { } }, docco: { - dev: { - src: [ - 'component/**/*.js', 'compute/**/*.js', 'construct/**/*.js', 'control/**/*.js', 'list/**/*.js', - 'map/**/*.js', 'model/**/*.js', 'observe/**/*.js','route/**/*.js', 'util/**/*.js','view/**/*.js', - '!util/dojo/dojo-1.8.1.js', '!util/dojo/nodelist-traverse.js','!**/*_test.js' + options: { + dst: 'docco/', + layout : 'parallel', + css : 'resources/docco.css' + }, + docs: { + files : [ + { + src : [ + 'component/**/*.js', 'compute/**/*.js', 'construct/**/*.js', 'control/**/*.js', 'list/**/*.js', + 'map/**/*.js', 'model/**/*.js', 'observe/**/*.js','route/**/*.js', 'util/**/*.js','view/**/*.js', + '!util/dojo/dojo-1.8.1.js', '!util/dojo/nodelist-traverse.js','!**/*_test.js' + ], + expand : true + } ], - options: { - output: 'docco/' - } } }, plato: { @@ -450,7 +457,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('bitovi-tools'); grunt.loadNpmTasks('grunt-jsbeautifier'); - grunt.loadNpmTasks('grunt-docco'); + grunt.loadNpmTasks('grunt-docco2'); grunt.loadNpmTasks('grunt-plato'); grunt.registerTask('quality', [ 'jsbeautifier', 'jshint']); diff --git a/component/component.md b/component/component.md index 3ffb1e06bbd..60a84375dff 100644 --- a/component/component.md +++ b/component/component.md @@ -3,7 +3,7 @@ @test can/component/test.html @parent canjs @release 2.0 -@link ../docco/component.html docco +@link ../docco/component/component.html docco @description Create widgets that use a template, a view-model diff --git a/compute/compute.js b/compute/compute.js index 798b601f172..4a135ffe28c 100644 --- a/compute/compute.js +++ b/compute/compute.js @@ -1,39 +1,55 @@ /* jshint maxdepth:7*/ + +// # can.compute +// +// `can.compute` allows creation of observable values +// from the result of a funciton. Any time an observable +// value that the function depends on changes, the +// function automatically updates. This enables creating +// observable data that relies on other sources, potentially +// multiple different ones. For instance, a `can.compute` is +// able to: +// - Combine a first and last name into a full name and update when either changes +// - Calculate the absolute value of an observable number, updating any time the observable number does +// - Specify complicated behavior for getting and setting a value, as well as how to handle changes + steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { - // # can.compute - // ## Reading Helpers // - // The following methods are used to call a function and know which observable events - // to listen to for changes. This is done by having every observable - // method that reads a value "broadcast" the corresponding - // event by calling `can.__reading(obserable, event)`. + // The following methods are used to call a function that relies on + // observable data and to track the observable events which should + // be listened to when changes occur. + // To do this, [`can.__reading(observable, event)`](#can-__reading) is called to + // "broadcast" the corresponding event on each read. // // ### Observed // - // An "Observed" is an Object of observable objects and events that - // need to be listened to to know when to check a function for updates. - // It looks like + // An "Observed" is an object of observable objects and events that + // a function relies on. These objects and events must be listened to + // in order to determine when to check a function for updates. + // This looks like the following: // // { - // "map1|first": {obs: map, event: "first"}, - // "map1|last" : {obs: map, event: "last"} + // "map1|first": {obj: map, event: "first"}, + // "map1|last" : {obj: map, event: "last"} // } // - // Each pair is mapped so no duplicates will be listed. - // + // Each object-event pair is mapped so no duplicates will be listed. + // ### State // - // `can.__read` can call a function that ends up calling `can.__read` again. For example, - // a compute can read another compute. - // To make sure we know each compute's "Observed" values, maintain a stack of - // each `__read` call's Observed valeus. + // `can.__read` may call a function that calls `can.__read` again. For + // example, a compute can read another compute. To track each compute's + // `Observed` object (containing observable objects and events), we maintain + // a stack of Observed values for each call to `__read`. var stack = []; - // Calls a function given a context and returns - // the return value of the function and the observable properties and events - // that were read. Example: `{value: 100, observed: Observed}` + // ### can.__read + // + // With a given function and context, calls the function + // and returns the resulting value of the function as well + // as the observable properties and events that were read. can.__read = function (func, self) { // Add an object that `can.__read` will write to. @@ -41,17 +57,22 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { var value = func.call(self); + // Example return value: + // `{value: 100, observed: Observed}` return { value: value, observed: stack.pop() }; }; - // When an observable value is read, it should call `can.__reading` to - // indicate which object and event should be listened to. + // ### can.__reading + // + // When an observable value is read, it must call `can.__reading` to + // broadcast which object and event should be listened to. can.__reading = function (obj, event) { - // Add the observe and attr that was read - // to `observed` + // Add the observable object and the event + // that was read to the `Observed` object on + // the stack. if (stack.length) { stack[stack.length-1][obj._cid + '|' + event] = { obj: obj, @@ -60,7 +81,12 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { } }; + + // ### can.__clearReading + // // Clears and returns the current observables. + // This can be used to access a value without + // it being handled as a regular `read`. can.__clearReading = function () { if (stack.length) { var ret = stack[stack.length-1]; @@ -68,7 +94,7 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { return ret; } }; - // Specifies reading values. + // Specifies current observables. can.__setReading = function (o) { if (stack.length) { stack[stack.length-1] = o; @@ -80,29 +106,36 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { } }; - // Calls a function, and using it's "Observed", sets up bindings to call - // `onchanged` when those events are triggered. + // ## Section Name + + // ### getValueAndBind + // + // Calls a function and sets up bindings to call `onchanged` + // when events from its "Observed" object are triggered. + // Removes bindings from `oldObserved` that are no longer needed. // - func - the function to call. // - context - the `this` of the function. - // - oldObserved - An object that contains what has been bound to - // - onchanged - what to call when any change has happened + // - oldObserved - an object that contains what has already been bound to + // - onchanged - the function to call when any change occurs var getValueAndBind = function (func, context, oldObserved, onchanged) { - // Call the function, get the value and the observeds. + // Call the function, get the value as well as the observed objects and events var info = can.__read(func, context), - // What needs to beound to. + // The objects-event pairs that must be bound to newObserveSet = info.observed, - // A flag that is used to figure out if we are already observing on an event. + // A flag that is used to determine if an event is already being observed. obEv, name; // Go through what needs to be observed. for( name in newObserveSet ) { if( oldObserved[name] ) { - // If name has already been observed, remove from - // `oldObserved` to prevent event from being unbound later. + // After binding is set up, values + // in `oldObserved` will be unbound. So if a name + // has already be observed, remove from `oldObserved` + // to prevent this. delete oldObserved[name]; } else { - // If this has not been observed, listen to it. + // If current name has not been observed, listen to it. obEv = newObserveSet[name]; obEv.obj.bind(obEv.event, onchanged); } @@ -118,8 +151,11 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { return info; }; + // ### updateOnChange + // + // Fires a change event when a compute's value changes var updateOnChange = function(compute, newValue, oldValue, batchNum){ - //console.log("update",compute._cid, newValue, oldValue) + // Only trigger event when value has changed if (newValue !== oldValue) { can.batch.trigger(compute, batchNum ? {type: "change", batchNum: batchNum} : 'change', [ newValue, @@ -127,24 +163,34 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { ]); } }; - // + + // ###setupComputeHandlers + // + // Sets up handlers for a compute. + // - compute - the compute to set up handlers for + // - func - the getter/setter function for the compute + // - context - the `this` for the compute + // - setCachedValue - function for setting cached value + // + // Returns an object with `on` and `off` functions. var setupComputeHandlers = function(compute, func, context, setCachedValue) { - var readInfo, onchanged, batchNum; return { + // Set up handler for when the compute changes on: function(updater){ if(!onchanged) { onchanged = function(ev){ if (compute.bound && (ev.batchNum === undefined || ev.batchNum !== batchNum) ) { - // store the old value + // Keep the old value var oldValue = readInfo.value; - // get the new value + // Get the new value readInfo = getValueAndBind(func, context, readInfo.observed, onchanged); + // Call the updater with old and new values updater(readInfo.value, oldValue, ev.batchNum); batchNum = batchNum = ev.batchNum; @@ -158,6 +204,7 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { compute.hasDependencies = !can.isEmptyObject(readInfo.observed); }, + // Remove handler for the compute off: function(updater){ for (var name in readInfo.observed) { var ob = readInfo.observed[name]; @@ -167,36 +214,51 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { }; }; + // ###isObserve + // + // Checks if an object is observable var isObserve = function (obj) { return obj instanceof can.Map || obj && obj.__get; }, + // Instead of calculating whether anything is listening every time, + // use a function to do nothing (which may be overwritten) k = function () {}; - // if no one is listening ... we can not calculate every time + + // ## Creating a can.compute + // + // A `can.compute` can be created by + // - [Specifying the getterSeter function](#specifying-gettersetter-function) + // - [Observing a property of an object](#observing-a-property-of-an-object) + // - [Specifying an initial value and a setter function](#specifying-an-initial-value-and-a-setter) + // - [Specifying an initial value and how to read, update, and listen to changes](#specifying-an-initial-value-and-a-settings-object) + // - [Simply specifying an initial value](#specifying-only-a-value) can.compute = function (getterSetter, context, eventName) { - + // ### Setting up + // Do nothing if getterSetter is already a compute if (getterSetter && getterSetter.isComputed) { return getterSetter; } - // the computed object + // The computed object var computed, // The following functions are overwritten depending on how compute() is called - // a method to setup listening + // A method to set up listening on = k, - // a method to teardown listening + // A method to teardown listening off = k, - // the current cached value (only valid if bound = true) + // Current cached value (valid only when bound is true) value, - // how to read the value + // How the value is read by default get = function () { return value; }, - // sets the value + // How the value is set by default set = function (newVal) { value = newVal; }, setCached = set, - // save for clone + // Save arguments for cloning args = can.makeArray(arguments), + // updater for when value is changed updater = function (newValue, oldValue, batchNum) { setCached(newValue); updateOnChange(computed, newValue,oldValue, batchNum); @@ -204,32 +266,36 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { // the form of the arguments form; computed = function (newVal) { - // setting ... + // If the computed function is called with arguments, + // a value should be set if (arguments.length) { - // save a reference to the old value + // Save a reference to the old value var old = value; - // setter may return a value if - // setter is for a value maintained exclusively by this compute + // Setter may return the value if setter + // is for a value maintained exclusively by this compute. var setVal = set.call(context, newVal, old); - // if this has dependencies return the current value + // If the computed function has dependencies, + // return the current value if (computed.hasDependencies) { return get.call(context); } + // Setting may not fire a change event, in which case + // the value must be read if (setVal === undefined) { - // it's possible, like with the DOM, setting does not - // fire a change event, so we must read value = get.call(context); } else { value = setVal; } - // fire the change + // Fire the change updateOnChange(computed, value, old); return value; } else { - // Another compute wants to bind to this compute + // Another compute may bind to this `computed` if (stack.length && computed.canReadForChangeEvent !== false) { // Tell the compute to listen to change on this computed + // Use `can.__reading` to allow other compute to listen + // for a change on this `computed` can.__reading(computed, 'change'); // We are going to bind on this compute. // If we are not bound, we should bind so that @@ -238,7 +304,7 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { can.compute.temporarilyBind(computed); } } - // if we are bound, use the cached value + // If computed is bound, use the cached value if (computed.bound) { return value; } else { @@ -246,7 +312,12 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { } } }; + // ###Specifying getterSetter function + // + // If `can.compute` is [called with a getterSetter function](http://canjs.com/docs/can.compute.html#sig_can_compute_getterSetter__context__), + // override set and get if (typeof getterSetter === 'function') { + // `can.compute(getterSetter, [context])` set = getterSetter; get = getterSetter; computed.canReadForChangeEvent = eventName === false ? false : true; @@ -254,7 +325,13 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { var handlers = setupComputeHandlers(computed, getterSetter, context || this, setCached); on = handlers.on; off = handlers.off; - + + // ###Observing a property of an object + // + // If `can.compute` is called with an + // [object, property name, and optional event name](http://canjs.com/docs/can.compute.html#sig_can_compute_object_propertyName__eventName__), + // create a compute from a property of an object. This allows the + // creation of a compute on objects that can be listened to with [`can.bind`](http://canjs.com/docs/can.bind.html) } else if (context) { if (typeof context === 'string') { // `can.compute(obj, "propertyName", [eventName])` @@ -263,6 +340,8 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { if (isObserve) { computed.hasDependencies = true; } + // If object is observable, `attr` will be used + // for getting and setting. get = function () { if (isObserve) { return getterSetter.attr(propertyName); @@ -292,6 +371,10 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { off = function () { can.unbind.call(getterSetter, eventName || propertyName, handler); }; + // ###Specifying an initial value and a setter + // + // If `can.compute` is called with an [initial value and a setter function](http://canjs.com/docs/can.compute.html#sig_can_compute_initialValue_setter_newVal_oldVal__), + // a compute that can adjust incoming values is set up. } else { // `can.compute(initialValue, setter)` if (typeof context === 'function') { @@ -300,7 +383,13 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { set = context; context = eventName; form = 'setter'; - + // ###Specifying an initial value and a settings object + // + // If `can.compute` is called with an [initial value and optionally a settings object](http://canjs.com/docs/can.compute.html#sig_can_compute_initialValue__settings__), + // a can.compute is created that can optionally specify how to read, + // update, and listen to changes in dependent values. This form of + // can.compute can be used to derive a compute that derives its + // value from any source } else { // `can.compute(initialValue,{get:, set:, on:, off:})` @@ -354,8 +443,12 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { off = options.off || off; } } + // ###Specifying only a value + // + // If can.compute is called with an initialValue only, + // reads to this value can be observed. } else { - // `can.compute(5)` + // `can.compute(initialValue)` value = getterSetter; } can.cid(computed, 'compute'); @@ -368,10 +461,11 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { isComputed: true, _bindsetup: function () { this.bound = true; - // setup live-binding - // while binding, this does not count as a read + // Set up live-binding + // While binding, this should not count as a read var oldReading = can.__clearReading(); on.call(this, updater); + // Restore "Observed" for reading can.__setReading(oldReading); }, _bindteardown: function () { @@ -429,7 +523,7 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { } }); }; - // a list of temporarily bound computes + // A list of temporarily bound computes var computes, unbindComputes = function () { for (var i = 0, len = computes.length; i < len; i++) { computes[i].unbind('change', k); @@ -446,6 +540,7 @@ steal('can/util', 'can/util/bind', 'can/util/batch', function (can, bind) { computes.push(compute); }; + // Whether a compute is truthy can.compute.truthy = function (compute) { return can.compute(function () { var res = compute(); diff --git a/compute/compute.md b/compute/compute.md index 4962acf77d6..12f5cf54cf8 100644 --- a/compute/compute.md +++ b/compute/compute.md @@ -1,7 +1,7 @@ @function can.compute @parent canjs @release 1.1 -@link ../docco/compute.html docco +@link ../docco/compute/compute.html docco @description Create an observable value. diff --git a/construct/construct.md b/construct/construct.md index 108b8c7f464..9ba78f4c39f 100644 --- a/construct/construct.md +++ b/construct/construct.md @@ -3,7 +3,7 @@ @test can/construct/test.html @parent canjs @group can.Construct.plugins plugins -@link ../docco/construct.html docco +@link ../docco/construct/construct.html docco @description diff --git a/control/control.js b/control/control.js index 062ab7302ac..80661c158d1 100644 --- a/control/control.js +++ b/control/control.js @@ -1,9 +1,14 @@ -steal('can/util', 'can/construct', function (can) { - // ## control.js - // `can.Control` - // _Controller_ +// # can/control/control.js +// +// Create organized, memory-leak free, rapidly performing, stateful +// controls with declarative eventing binding. Used when creating UI +// controls with behaviors, bound to elements on the page. +// ## helpers - // Binds an element, returns a function that unbinds. +steal('can/util', 'can/construct', function (can) { + // + // ### bind + // this helper binds to one element and returns a function that unbinds from that element. var bind = function (el, ev, callback) { can.bind.call(el, ev, callback); @@ -19,7 +24,10 @@ steal('can/util', 'can/construct', function (can) { paramReplacer = /\{([^\}]+)\}/g, special = can.getObject("$.event.special", [can]) || {}, - // Binds an element, returns a function that unbinds. + // ### delegate + // + // this helper binds to elements based on a selector and returns a + // function that unbinds. delegate = function (el, selector, ev, callback) { can.delegate.call(el, selector, ev, callback); return function () { @@ -27,6 +35,8 @@ steal('can/util', 'can/construct', function (can) { }; }, + // ### binder + // // Calls bind or unbind depending if there is a selector. binder = function (el, ev, callback, selector) { return selector ? @@ -40,32 +50,24 @@ steal('can/util', 'can/construct', function (can) { /** * @add can.Control */ - // + // ## *static functions* /** * @static */ { - // Setup pre-processes which methods are event listeners. - /** - * @hide - * - * Setup pre-process which methods are event listeners. - * - */ + // ## can.Control.setup + // + // This function pre-processes which methods are event listeners and which are methods of + // the control. It has a mechanism to allow controllers to inherit default values from super + // classes, like `can.Construct`, and will cache functions that are action functions (see `_isAction`) + // or functions with an underscored name. setup: function () { - - // Allow contollers to inherit "defaults" from super-classes as it - // done in `can.Construct` can.Construct.setup.apply(this, arguments); - // If you didn't provide a name, or are `control`, don't do anything. if (can.Control) { - - // Cache the underscored names. var control = this, funcName; - // Calculate and cache actions. control.actions = {}; for (funcName in control.prototype) { if (control._isAction(funcName)) { @@ -74,7 +76,10 @@ steal('can/util', 'can/construct', function (can) { } } }, - // Moves `this` to the first argument, wraps it with `jQuery` if it's an element + // ## can.Control._shifter + // + // Moves `this` to the first argument, wraps it with `jQuery` if it's + // an element. _shifter: function (context, name) { var method = typeof name === "string" ? context[name] : name; @@ -89,55 +94,35 @@ steal('can/util', 'can/construct', function (can) { }; }, - // Return `true` if is an action. - /** - * @hide - * @param {String} methodName a prototype function - * @return {Boolean} truthy if an action or not - */ + // ## can.Control._isAction + // + // Return `true` if `methodName` refers to an action. An action is a `methodName` value that + // is not the constructor, and is either a function or string that refers to a function, or is + // defined in `special`, `processors`. Detects whether `methodName` is also a valid method name. _isAction: function (methodName) { - var val = this.prototype[methodName], type = typeof val; - // if not the constructor + return (methodName !== 'constructor') && - // and is a function or links to a function (type === "function" || (type === "string" && isFunction(this.prototype[val]))) && - // and is in special, a processor, or has a funny character !! (special[methodName] || processors[methodName] || /[^\w]/.test(methodName)); }, - // Takes a method name and the options passed to a control - // and tries to return the data necessary to pass to a processor - // (something that binds things). - /** - * @hide - * Takes a method name and the options passed to a control - * and tries to return the data necessary to pass to a processor - * (something that binds things). - * - * For performance reasons, this called twice. First, it is called when - * the Control class is created. If the methodName is templated - * like: "{window} foo", it returns null. If it is not templated - * it returns event binding data. - * - * The resulting data is added to this.actions. - * - * When a control instance is created, _action is called again, but only - * on templated actions. - * - * @param {Object} methodName the method that will be bound - * @param {Object} [options] first param merged with class default options - * @return {Object} null or the processor and pre-split parts. - * The processor is what does the binding/subscribing. - */ - _action: function (methodName, options, controlInstance ) { - - // If we don't have options (a `control` instance), we'll run this - // later. + // ## can.Control._action + // + // Takes a method name and the options passed to a control and tries to return the data + // necessary to pass to a processor (something that binds things). + // + // For performance reasons, `_action` is called twice: + // * It's called when the Control class is created. for templated method names (e.g., `{window} foo`), it returns null. For non-templated method names it returns the event binding data. That data is added to `this.actions`. + // * It is called wehn a control instance is created, but only for templated actions. + _action: function (methodName, options) { + + // If we don't have options (a `control` instance), we'll run this later. If we have + // options, run `can.sub` to replace the action template `{}` with values from the `options` + // or `window`. If a `{}` template resolves to an object, `convertedName` will be an array. + // In that case, the event name we want will be the last item in that array. paramReplacer.lastIndex = 0; if (options || !paramReplacer.test(methodName)) { - // If we have options, run sub to replace templates `{}` with a - // value from the options or the window var convertedName = options ? can.sub(methodName, this._lookup(options)) : methodName; if (!convertedName) { //!steal-remove-start @@ -145,14 +130,8 @@ steal('can/util', 'can/construct', function (can) { //!steal-remove-end return null; } - // If a `{}` template resolves to an object, `convertedName` will be - // an array var arr = can.isArray(convertedName), - - // Get the name name = arr ? convertedName[1] : convertedName, - - // Grab the event off the end parts = name.split(/\s+/g), event = parts.pop(); @@ -166,505 +145,72 @@ steal('can/util', 'can/construct', function (can) { _lookup: function (options) { return [options, window]; }, + // ## can.Control.processors + // // An object of `{eventName : function}` pairs that Control uses to - // hook up events auto-magically. - /** - * @property {Object.} can.Control.processors processors - * @parent can.Control.static - * - * @description A collection of hookups for custom events on Controls. - * - * @body - * `processors` is an object that allows you to add new events to bind - * to on a control, or to change how existent events are bound. Each - * key-value pair of `processors` is a specification that pertains to - * an event where the key is the name of the event, and the value is - * a function that processes calls to bind to the event. - * - * The processor function takes five arguments: - * - * - _el_: The Control's element. - * - _event_: The event type. - * - _selector_: The selector preceding the event in the binding used on the Control. - * - _callback_: The callback function being bound. - * - _control_: The Control the event is bound on. - * - * Inside your processor function, you should bind _callback_ to the event, and - * return a function for can.Control to call when _callback_ needs to be unbound. - * (If _selector_ is defined, you will likely want to use some form of delegation - * to bind the event.) - * - * Here is a Control with a custom event processor set and two callbacks bound - * to that event: - * - * @codestart - * can.Control.processors.birthday = function(el, ev, selector, callback, control) { - * if(selector) { - * myFramework.delegate(ev, el, selector, callback); - * return function() { myFramework.undelegate(ev, el, selector, callback); }; - * } else { - * myFramework.bind(ev, el, callback); - * return function() { myFramework.unbind(ev, el, callback); }; - * } - * }; - * - * can.Control("EventTarget", { }, { - * 'birthday': function(el, ev) { - * // do something appropriate for the occasion - * }, - * '.grandchild birthday': function(el, ev) { - * // do something appropriate for the occasion - * } - * }); - * - * var target = new EventTarget('#person'); - * @codeend - * - * When `target` is initialized, can.Control will call `can.Control.processors.birthday` - * twice (because there are two event hookups for the _birthday_ event). The first - * time it's called, the arguments will be: - * - * - _el_: A NodeList that wraps the element with id 'person'. - * - _ev_: `'birthday'` - * - _selector_: `''` - * - _callback_: The function assigned to `' birthday'` in the prototype section of `EventTarget`'s - * definition. - * - _control_: `target` itself. - * - * The second time, the arguments are slightly different: - * - * - _el_: A NodeList that wraps the element with id 'person'. - * - _ev_: `'birthday'` - * - _selector_: `'.grandchild'` - * - _callback_: The function assigned to `'.grandchild birthday'` in the prototype section of `EventTarget`'s - * definition. - * - _control_: `target` itself. - * - * can.Control already has processors for these events: - * - * - change - * - click - * - contextmenu - * - dblclick - * - focusin - * - focusout - * - keydown - * - keyup - * - keypress - * - mousedown - * - mouseenter - * - mouseleave - * - mousemove - * - mouseout - * - mouseover - * - mouseup - * - reset - * - resize - * - scroll - * - select - * - submit - */ + // hook up events automatically. processors: {}, - // A object of name-value pairs that act as default values for a - // control instance + // ## can.Control.defaults + // A object of name-value pairs that act as default values for a control instance defaults: {} - /** - * @property {Object} can.Control.defaults defaults - * @parent can.Control.static - * @description Default values for the Control's options. - * - * @body - * `defaults` provides default values for a Control's options. - * Options passed into the constructor function will be shallowly merged - * into the values from defaults in [can.Control::setup], and - * the result will be stored in [can.Control::options this.options]. - * - * Message = can.Control.extend({ - * defaults: { - * message: "Hello World" - * } - * }, { - * init: function(){ - * this.element.text( this.options.message ); - * } - * }); - * - * new Message( "#el1" ); //writes "Hello World" - * new Message( "#el12", { message: "hi" } ); //writes hi - */ }, { + // ## *prototype functions* /** * @prototype */ - // - /** - * @functioncan.Control.prototype.init init - * @parent can.Control.prototype - * @description instance init method required for most applications of [can.Control] - * @signature `control.init(element,options)` - * @param element The wrapped element passed to the control. - * Control accepts a raw HTMLElement, a CSS selector, or a NodeList. - * This is set as `this.element` on the control instance. - * @param options The second argument passed to new Control, - * extended with the can.Control's static _defaults__. - * This is set as `this.options` on the control instance. - * Note that static is used formally to indicate that - * _default values are shared across control instances_. - * - * @body - * Any additional arguments provided to the constructor will be passed as normal. - */ - // Sets `this.element`, saves the control in `data, binds event - // handlers. - /** - * @property {NodeList} can.Control.prototype.element element - * @parent can.Control.prototype - * @description The element associated with this control. - * - * @body - * The library-wrapped element this control is associated with, - * as passed into the constructor. If you want to change the element - * that a Control will attach to, you should do it in [can.Control::setup setup]. - * If you change the element later, make sure to call [can.Control::on on] - * to rebind all the bindings. - * - * If `element` is removed from the DOM, [can.Control::destroy] will - * be called and the Control will be destroyed. - */ - // - /** - * @function can.Control.prototype.setup setup - * @parent can.Control.prototype - * @description Perform pre-initialization logic. - * @signature `control.setup(element, options)` - * @param {HTMLElement|NodeList|String} element The element as passed to the constructor. - * @param {Object} [options] option values for the control. These get added to - * this.options and merged with [can.Control.static.defaults defaults]. - * @return {undefined|Array} return an array if you want to change what init is called with. By - * default it is called with the element and options passed to the control. - * - * @body - * Setup is where most of control's magic happens. It does the following: - * - * ### Sets this.element - * - * The first parameter passed to new Control( el, options ) is expected to be - * an element. This gets converted to a Wrapped NodeList element and set as - * [can.Control.prototype.element this.element]. - * - * ### Adds the control's name to the element's className - * - * Control adds it's plugin name to the element's className for easier - * debugging. For example, if your Control is named "Foo.Bar", it adds - * "foo_bar" to the className. - * - * ### Saves the control in $.data - * - * A reference to the control instance is saved in $.data. You can find - * instances of "Foo.Bar" like: - * - * $( '#el' ).data( 'controls' )[ 'foo_bar' ] - * - * ### Merges Options - * Merges the default options with optional user-supplied ones. - * Additionally, default values are exposed in the static [can.Control.static.defaults defaults] - * so that users can change them. - * - * ### Binds event handlers - * - * Setup does the event binding described in [can.Control]. - */ + // ## setup + // + // Setup is where most of the Control's magic happens. It performs several pre-initialization steps: + // - Sets `this.element` + // - Adds the Control's name to the element's className + // - Saves the Control in `$.data` + // - Merges Options + // - Binds event handlers using `delegate` + // The final step is to return pass the element and prepareed options, to be used in `init`. setup: function (element, options) { var cls = this.constructor, pluginname = cls.pluginName || cls._fullName, arr; - // Want the raw element here. + // Retrieve the raw element, then set the plugin name as a class there. this.element = can.$(element); if (pluginname && pluginname !== 'can_control') { - // Set element and `className` on element. this.element.addClass(pluginname); } - // Set up the 'controls' data on the element + // Set up the 'controls' data on the element. If it does not exist, initialize + // it to an empty array. arr = can.data(this.element, 'controls'); if (!arr) { - // If it does not exist, initialize it to an empty array arr = []; can.data(this.element, 'controls', arr); } arr.push(this); - // Option merging. - /** - * @property {Object} can.Control.prototype.options options - * @parent can.Control.prototype - * - * @description - * - * Options used to configure a control. - * - * @body - * - * The `this.options` property is an Object that contains - * configuration data passed to a control when it is - * created (`new can.Control(element, options)`). - * - * In the following example, an options object with - * a message is passed to a `Greeting` control. The - * `Greeting` control changes the text of its [can.Control::element element] - * to the options' message value. - * - * var Greeting = can.Control.extend({ - * init: function(){ - * this.element.text( this.options.message ) - * } - * }) - * - * new Greeting("#greeting",{message: "I understand this.options"}) - * - * The options argument passed when creating the control - * is merged with [can.Control.defaults defaults] in - * [can.Control.prototype.setup setup]. - * - * In the following example, if no message property is provided, - * the defaults' message property is used. - * - * var Greeting = can.Control.extend({ - * defaults: { - * message: "Defaults merged into this.options" - * } - * },{ - * init: function(){ - * this.element.text( this.options.message ) - * } - * }) - * - * new Greeting("#greeting") - * - */ + // The `this.options` property is an Object that contains configuration data + // passed to a control when it is created (`new can.Control(element, options)`) + // + // The `options` argument passed when creating the control is merged with `can.Control.defaults` + // in [can.Control.prototype.setup setup]. + // + // If no `options` value is used during creation, the value in `defaults` is used instead this.options = extend({}, cls.defaults, options); - // Bind all event handlers. this.on(); - // Gets passed into `init`. - /** - * @property {can.NodeList} can.Control.prototype.element element - * - * @description The element the Control is associated with. - * - * @parent can.Control.prototype - * - * @body - * - * The control instance's HTMLElement (or window) wrapped by the - * util library for ease of use. It is set by the first - * parameter to `new can.Construct( element, options )` - * in [can.Control::setup]. By default, a control listens to events on `this.element`. - * - * ### Quick Example - * - * The following `HelloWorld` control sets the control`s text to "Hello World": - * - * HelloWorld = can.Control({ - * init: function(){ - * this.element.text( 'Hello World' ); - * } - * }); - * - * // create the controller on the element - * new HelloWorld( document.getElementById( '#helloworld' ) ); - * - * ## Wrapped NodeList - * - * `this.element` is a wrapped NodeList of one HTMLELement (or window). This - * is for convenience in libraries like jQuery where all methods operate only on a - * NodeList. To get the raw HTMLElement, write: - * - * this.element[0] //-> HTMLElement - * - * The following details the NodeList used by each library with - * an example of updating its text: - * - * __jQuery__ `jQuery( HTMLElement )` - * - * this.element.text("Hello World") - * - * __Zepto__ `Zepto( HTMLElement )` - * - * this.element.text("Hello World") - * - * __Dojo__ `new dojo.NodeList( HTMLElement )` - * - * this.element.text("Hello World") - * - * __Mootools__ `$$( HTMLElement )` - * - * this.element.empty().appendText("Hello World") - * - * __YUI__ - * - * this.element.set("text", "Hello World") - * - * - * ## Changing `this.element` - * - * Sometimes you don't want what's passed to `new can.Control` - * to be this.element. You can change this by overwriting - * setup or by unbinding, setting this.element, and rebinding. - * - * ### Overwriting Setup - * - * The following Combobox overwrites setup to wrap a - * select element with a div. That div is used - * as `this.element`. Notice how `destroy` sets back the - * original element. - * - * Combobox = can.Control({ - * setup: function( el, options ) { - * this.oldElement = $( el ); - * var newEl = $( '
' ); - * this.oldElement.wrap( newEl ); - * can.Control.prototype.setup.call( this, newEl, options ); - * }, - * init: function() { - * this.element //-> the div - * }, - * ".option click": function() { - * // event handler bound on the div - * }, - * destroy: function() { - * var div = this.element; //save reference - * can.Control.prototype.destroy.call( this ); - * div.replaceWith( this.oldElement ); - * } - * }); - * - * ### unbinding, setting, and rebinding. - * - * You could also change this.element by calling - * [can.Control::off], setting this.element, and - * then calling [can.Control::on] like: - * - * move: function( newElement ) { - * this.off(); - * this.element = $( newElement ); - * this.on(); - * } - */ return [this.element, this.options]; }, - /** - * @function can.Control.prototype.on on - * @parent can.Control.prototype - * - * @description Bind an event handler to a Control, or rebind all event handlers on a Control. - * - * @signature `control.on([el,] selector, eventName, func)` - * @param {HTMLElement|jQuery collection|Object} [el=this.element] - * The element to be bound. If no element is provided, the control's element is used instead. - * @param {CSSSelectorString} selector A CSS selector for event delegation. - * @param {String} eventName The name of the event to listen for. - * @param {Function|String} func A callback function or the String name of a control function. If a control - * function name is given, the control function is called back with the bound element and event as the first - * and second parameter. Otherwise the function is called back like a normal bind. - * @return {Number} The id of the binding in this._bindings. - * - * @body - * `on(el, selector, eventName, func)` binds an event handler for an event to a selector under the scope of the given element. - * - * @signature `control.on()` - * - * Rebind all of a control's event handlers. - * - * @return {Number} The number of handlers bound to this Control. - * - * @body - * `this.on()` is used to rebind - * all event handlers when [can.Control::options this.options] has changed. It - * can also be used to bind or delegate from other elements or objects. - * - * ## Rebinding - * - * By using templated event handlers, a control can listen to objects outside - * `this.element`. This is extremely common in MVC programming. For example, - * the following control might listen to a task model's `completed` property and - * toggle a strike className like: - * - * TaskStriker = can.Control({ - * "{task} completed": function(){ - * this.update(); - * }, - * update: function(){ - * if ( this.options.task.completed ) { - * this.element.addClass( 'strike' ); - * } else { - * this.element.removeClass( 'strike' ); - * } - * } - * }); - * - * var taskstriker = new TaskStriker({ - * task: new Task({ completed: 'true' }) - * }); - * - * To update the `taskstriker`'s task, add a task method that updates - * this.options and rebinds the event handlers for the new task like: - * - * TaskStriker = can.Control({ - * "{task} completed": function(){ - * this.update(); - * }, - * update: function() { - * if ( this.options.task.completed ) { - * this.element.addClass( 'strike' ); - * } else { - * this.element.removeClass( 'strike' ); - * } - * }, - * task: function( newTask ) { - * this.options.task = newTask; - * this.on(); - * this.update(); - * } - * }); - * - * var taskstriker = new TaskStriker({ - * task: new Task({ completed: true }) - * }); - * - * // Now, add a new task that is not yet completed - * taskstriker.task(new Task({ completed: false })); - * - * ## Adding new events - * - * If events need to be bound to outside of the control and templated event handlers - * are not sufficient, you can call this.on to bind or delegate programmatically: - * - * init: function() { - * // calls somethingClicked( el, ev ) - * this.on( 'click', 'somethingClicked' ); - * - * // calls function when the window is clicked - * this.on( window, 'click', function( ev ) { - * //do something - * }); - * }, - * somethingClicked: function( el, ev ) { - * // ... - * } - */ + // ## on + // + // This binds an event handler for an event to a selector under the scope of `this.element` + // If no options are specified, all events are rebound to their respective elements. The actions, + // which were cached in `setup`, are used and all elements are bound using `delegate` from `this.element`. on: function (el, selector, eventName, func) { if (!el) { - - // Adds bindings. this.off(); - // Go through the cached list of actions and use the processor - // to bind var cls = this.constructor, bindings = this._bindings, actions = cls.actions, @@ -683,8 +229,7 @@ steal('can/util', 'can/construct', function (can) { } } - // Setup to be destroyed... - // don't bind because we don't want to remove it. + // Set up the ability to `destroy` the control later. can.bind.call(element, "removed", destroyCB); bindings.user.push(function (el) { can.unbind.call(el, "removed", destroyCB); @@ -715,12 +260,10 @@ steal('can/util', 'can/construct', function (can) { return this._bindings.user.length; }, + // ## off + // // Unbinds all event handlers on the controller. - /** - * @hide - * Unbinds all event handlers on the controller. You should never - * be calling this unless in use with [can.Control::on]. - */ + // This should _only_ be called in combination with .on() off: function () { var el = this.element[0], bindings = this._bindings; @@ -735,110 +278,12 @@ steal('can/util', 'can/construct', function (can) { // Adds bindings. this._bindings = {user: [], control: {}}; }, - // Prepares a `control` for garbage collection - /** - * @description Remove a Control from an element and clean up the Control. - * @signature `control.destroy()` - * - * Prepares a control for garbage collection and is a place to - * reset any changes the control has made. - * - * @function can.Control.prototype.destroy destroy - * @parent can.Control.prototype - * - * @body - * - * - * ## Allowing Garbage Collection - * - * Destroy is called whenever a control's element is removed from the page using - * the library's standard HTML modifier methods. This means that you - * don't have to call destroy yourself and it - * will be called automatically when appropriate. - * - * The following `Clicker` widget listens on the window for clicks and updates - * its element's innerHTML. If we remove the element, the window's event handler - * is removed auto-magically: - * - * - * Clickr = can.Control({ - * "{window} click": function() { - * this.element.html( this.count ? - * this.count++ : this.count = 0 ); - * } - * }); - * - * // create a clicker on an element - * new Clicker( "#clickme" ); - * - * // remove the element - * $( '#clickme' ).remove(); - * - * - * The methods you can use that will destroy controls automatically by library: - * - * __jQuery and Zepto__ - * - * - $.fn.remove - * - $.fn.html - * - $.fn.replaceWith - * - $.fn.empty - * - * __Dojo__ - * - * - dojo.destroy - * - dojo.empty - * - dojo.place (with the replace option) - * - * __Mootools__ - * - * - Element.prototype.destroy - * - * __YUI__ - * - * - Y.Node.prototype.remove - * - Y.Node.prototype.destroy - * - * - * ## Teardown in Destroy - * - * Sometimes, you want to reset a controlled element back to its - * original state when the control is destroyed. Overwriting destroy - * lets you write teardown code of this manner. __When overwriting - * destroy, make sure you call Control's base functionality__. - * - * The following example changes an element's text when the control is - * created and sets it back when the control is removed: - * - * Changer = can.Control.extend({ - * init: function() { - * this.oldText = this.element.text(); - * this.element.text( "Changed!!!" ); - * }, - * destroy: function() { - * this.element.text( this.oldText ); - * can.Control.prototype.destroy.call( this ); - * } - * }); - * - * // create a changer which changes #myel's text - * var changer = new Changer( '#myel' ); - * - * // destroy changer which will reset it - * changer.destroy(); - * - * ## Base Functionality - * - * Control prepares the control for garbage collection by: - * - * - unbinding all event handlers - * - clearing references to this.element and this.options - * - clearing the element's reference to the control - * - removing it's [can.Control.pluginName] from the element's className - * - */ + // ## destroy + // + // Prepares a `control` for garbage collection. + // First checks if it has already been removed. Then, removes all the bindings, data, and + // the element from the Control instance. destroy: function () { - //Control already destroyed if (this.element === null) { //!steal-remove-start can.dev.warn("can/control/control.js: Control already destroyed"); @@ -849,29 +294,26 @@ steal('can/util', 'can/construct', function (can) { pluginName = Class.pluginName || Class._fullName, controls; - // Unbind bindings. this.off(); if (pluginName && pluginName !== 'can_control') { - // Remove the `className`. this.element.removeClass(pluginName); } - // Remove from `data`. controls = can.data(this.element, "controls"); controls.splice(can.inArray(this, controls), 1); - can.trigger(this, "destroyed"); // In case we want to know if the `control` is removed. + can.trigger(this, "destroyed"); this.element = null; } }); + // ## Processors + // + // Processors do the binding. This basic processor binds events. Each returns a function that unbinds + // when called. var processors = can.Control.processors; - // Processors do the binding. - // They return a function that unbinds when called. - // - // The basic processor that binds events. basicProcessor = function (el, event, selector, methodName, control) { return binder(el, event, can.Control._shifter(control, methodName), selector); }; @@ -881,8 +323,6 @@ steal('can/util', 'can/construct', function (can) { "keypress", "mousedown", "mousemove", "mouseout", "mouseover", "mouseup", "reset", "resize", "scroll", "select", "submit", "focusin", "focusout", "mouseenter", "mouseleave", - // #104 - Add touch events as default processors - // TOOD feature detect? "touchstart", "touchmove", "touchcancel", "touchend", "touchleave" ], function (v) { processors[v] = basicProcessor; diff --git a/control/control.md b/control/control.md index 23db52ec341..ab61e892492 100644 --- a/control/control.md +++ b/control/control.md @@ -6,7 +6,7 @@ @inherits can.Construct @description widget factory with declarative event binding. @group can.Control.plugins plugins -@link ../docco/control.html docco +@link ../docco/control/control.html docco @description Create organized, memory-leak free, rapidly performing, stateful controls with declarative event binding. Use `can.Control` to create UI diff --git a/control/doc/defaults.md b/control/doc/defaults.md new file mode 100644 index 00000000000..78750b03006 --- /dev/null +++ b/control/doc/defaults.md @@ -0,0 +1,25 @@ +@property {Object} can.Control.defaults defaults +@parent can.Control.static +@description Default values for the Control's options. + +@body + +Default options provided for when a new control is created without values set in `options`. + +`defaults` provides default values for a Control's options. +Options passed into the constructor function will be shallowly merged +into the values from defaults in [can.Control::setup], and +the result will be stored in [can.Control::options this.options]. + + Message = can.Control.extend({ + defaults: { + message: "Hello World" + } + }, { + init: function(){ + this.element.text( this.options.message ); + } + }); + + new Message( "#el1" ); //writes "Hello World" + new Message( "#el12", { message: "hi" } ); //writes hi \ No newline at end of file diff --git a/control/doc/destroy.md b/control/doc/destroy.md new file mode 100644 index 00000000000..869cd822c3f --- /dev/null +++ b/control/doc/destroy.md @@ -0,0 +1,95 @@ +@function can.Control.prototype.destroy destroy +@parent can.Control.prototype +@description Remove a Control from an element and clean up the Control. +@signature `control.destroy()` + +Prepares a control for garbage collection and is a place to +reset any changes the control has made. + +@body +## Allowing Garbage Collection + +Destroy is called whenever a control's element is removed from the page using +the library's standard HTML modifier methods. This means that you +don't have to call destroy yourself and it +will be called automatically when appropriate. + +The following `Clicker` widget listens on the window for clicks and updates +its element's innerHTML. If we remove the element, the window's event handler +is removed auto-magically: + + + Clicker = can.Control({ + "{window} click": function() { + this.element.html( this.count ? + this.count++ : this.count = 0 ); + } + }); + + // create a clicker on an element + new Clicker( "#clickme" ); + + // remove the element + $( '#clickme' ).remove(); + +The methods you can use that will destroy controls automatically by library: + +__jQuery and Zepto__ + +- `$.fn.remove` +- `$.fn.html` +- `$.fn.replaceWith` +- `$.fn.empty` + +__Dojo__ + +- `dojo.destroy` +- `dojo.empty` +- `dojo.place (with the replace option)` + +__Mootools__ + +- `Element.prototype.destroy` + +__YUI__ + +- `Y.Node.prototype.remove` +- `Y.Node.prototype.destroy` + + +## Teardown in Destroy + +Sometimes, you want to reset a controlled element back to its +original state when the control is destroyed. Overwriting destroy +lets you write teardown code of this manner. + +__NOTE__: When overwriting destroy, make sure you call Control's base functionality. + +The following example changes an element's text when the control is +created and sets it back when the control is removed: + + Changer = can.Control.extend({ + init: function() { + this.oldText = this.element.text(); + this.element.text( "Changed!!!" ); + }, + destroy: function() { + this.element.text( this.oldText ); + can.Control.prototype.destroy.call( this ); + } + }); + + // create a changer which changes #myel's text + var changer = new Changer( '#myel' ); + + // destroy changer which will reset it + changer.destroy(); + +## Base Functionality + +Control prepares the control for garbage collection by: + +- unbinding all event handlers +- clearing references to this.element and this.options +- clearing the element's reference to the control +- removing it's `can.Control.pluginName` from the element's className diff --git a/control/doc/element.md b/control/doc/element.md new file mode 100644 index 00000000000..03658f5e904 --- /dev/null +++ b/control/doc/element.md @@ -0,0 +1,100 @@ +@property {can.NodeList} can.Control.prototype.element element +@parent can.Control.prototype +@description The element passed to the Control when creating a new instance. + +@body + +The control instance's HTMLElement (or window) wrapped by the +util library for ease of use. + +It is set by the first parameter to `new can.Construct( element, options )` +in [can.Control::setup]. By default, a control listens to events on `this.element`. + +### Example - NodeList + +The following `HelloWorld` control sets the control`s text to "Hello World": + + HelloWorld = can.Control({ + init: function(){ + this.element.text( 'Hello World' ); + } + }); + + // create the controller on the element + new HelloWorld( document.getElementById( '#helloworld' ) ); + +## Wrapped NodeList + +`this.element` is a wrapped NodeList of one HTMLELement (or window). This +is for convenience in libraries like jQuery where all methods operate only on a +NodeList. To get the raw HTMLElement, write: + + this.element[0] //-> HTMLElement + +The following details the NodeList used by each library with +an example of updating its text: + +__jQuery__ `jQuery( HTMLElement )` + + this.element.text("Hello World") + +__Zepto__ `Zepto( HTMLElement )` + + this.element.text("Hello World") + +__Dojo__ `new dojo.NodeList( HTMLElement )` + + this.element.text("Hello World") + +__Mootools__ `$$( HTMLElement )` + + this.element.empty().appendText("Hello World") + +__YUI__ + + this.element.set("text", "Hello World") + +## Changing `this.element` + +Sometimes you don't want what's passed to `new can.Control` +to be `this.element`. You can change this by overwriting +setup or by unbinding, setting this.element, and rebinding. + +### Overwriting Setup + +The following Combobox overwrites setup to wrap a +select element with a div. That div is used +as `this.element`. Notice how `destroy` sets back the +original element. + + Combobox = can.Control({ + setup: function( el, options ) { + this.oldElement = $( el ); + var newEl = $( '
' ); + this.oldElement.wrap( newEl ); + can.Control.prototype.setup.call( this, newEl, options ); + }, + init: function() { + this.element //-> the div + }, + ".option click": function() { + // event handler bound on the div + }, + destroy: function() { + var div = this.element; //save reference + can.Control.prototype.destroy.call( this ); + div.replaceWith( this.oldElement ); + } + }); + +### Unbinding, setting, and rebinding. + +You could also change this.element by calling +[can.Control::off], setting this.element, and +then calling [can.Control::on] like: + + move: function( newElement ) { + this.off(); + this.element = $( newElement ); + this.on(); + } \ No newline at end of file diff --git a/control/doc/on.md b/control/doc/on.md new file mode 100644 index 00000000000..5a837ac2725 --- /dev/null +++ b/control/doc/on.md @@ -0,0 +1,96 @@ +@function can.Control.prototype.on on +@parent can.Control.prototype +@description Bind an event handler to a Control, or rebind all event handlers on a Control. + +@signature `control.on([el,] selector, eventName, func)` +@param {HTMLElement|jQuery collection|Object} [el=this.element] +The element to be bound. If no element is provided, the control's element is used instead. +@param {CSSSelectorString} selector A CSS selector for event delegation. +@param {String} eventName The name of the event to listen for. +@param {Function|String} func A callback function or the String name of a control function. If a control +function name is given, the control function is called back with the bound element and event as the first +and second parameter. Otherwise the function is called back like a normal bind. +@return {Number} The id of the binding in this._bindings. + +`on(el, selector, eventName, func)` binds an event handler for an event to a selector under the scope of the given element. + +@signature `control.on()` + +Rebind all of a control's event handlers. + +@return {Number} The number of handlers bound to this Control. + +@body +`this.on()` is used to rebind +all event handlers when [can.Control::options this.options] has changed. It +can also be used to bind or delegate from other elements or objects. + +## Rebinding + +By using templated event handlers, a control can listen to objects outside +`this.element`. This is extremely common in MVC programming. For example, +the following control might listen to a task model's `completed` property and +toggle a strike className like: + + TaskStriker = can.Control({ + "{task} completed": function(){ + this.update(); + }, + update: function(){ + if ( this.options.task.completed ) { + this.element.addClass( 'strike' ); + } else { + this.element.removeClass( 'strike' ); + } + } + }); + + var taskstriker = new TaskStriker({ + task: new Task({ completed: 'true' }) + }); + +To update the `taskstriker`'s task, add a task method that updates +this.options and rebinds the event handlers for the new task like: + + TaskStriker = can.Control({ + "{task} completed": function(){ + this.update(); + }, + update: function() { + if ( this.options.task.completed ) { + this.element.addClass( 'strike' ); + } else { + this.element.removeClass( 'strike' ); + } + }, + task: function( newTask ) { + this.options.task = newTask; + this.on(); + this.update(); + } + }); + + var taskstriker = new TaskStriker({ + task: new Task({ completed: true }) + }); + + // Now, add a new task that is not yet completed + taskstriker.task(new Task({ completed: false })); + +## Adding new events + +If events need to be bound to outside of the control and templated event handlers +are not sufficient, you can call this.on to bind or delegate programmatically: + + init: function() { + // calls somethingClicked( el, ev ) + this.on( 'click', 'somethingClicked' ); + + // calls function when the window is clicked + this.on( window, 'click', function( ev ) { + // do something + }); + }, + somethingClicked: function( el, ev ) { + // ... + } \ No newline at end of file diff --git a/control/doc/options.md b/control/doc/options.md new file mode 100644 index 00000000000..794b6de5dea --- /dev/null +++ b/control/doc/options.md @@ -0,0 +1,41 @@ +@property {Object} can.Control.prototype.options options +@parent can.Control.prototype +@description Options used to configure a control. + +@body + +The `this.options` property is an Object that contains +configuration data passed to a control when it is +created (`new can.Control(element, options)`). + +In the following example, an options object with +a message is passed to a `Greeting` control. The +`Greeting` control changes the text of its [can.Control::element element] +to the options' message value. + + var Greeting = can.Control.extend({ + init: function(){ + this.element.text( this.options.message ) + } + }); + + new Greeting("#greeting",{message: "I understand this.options"}); + +The options argument passed when creating the control +is merged with [can.Control.defaults defaults] in +[can.Control.prototype.setup setup]. + +In the following example, if no message property is provided, +the defaults' message property is used. + + var Greeting = can.Control.extend({ + defaults: { + message: "Defaults merged into this.options" + } + },{ + init: function(){ + this.element.text( this.options.message ) + } + }); + + new Greeting("#greeting"); \ No newline at end of file diff --git a/control/doc/processors.md b/control/doc/processors.md new file mode 100644 index 00000000000..975c81fc4d0 --- /dev/null +++ b/control/doc/processors.md @@ -0,0 +1,91 @@ +@property {Object.} can.Control.processors processors +@parent can.Control.static +@description A collection of hookups for custom events on Controls. +@body + +`processors` is an object that allows you to add new events to bind +to on a control, or to change how existent events are bound. Each +key-value pair of `processors` is a specification that pertains to +an event where the key is the name of the event, and the value is +a function that processes calls to bind to the event. + +The processor function takes five arguments: + +- _el_: The Control's element. +- _event_: The event type. +- _selector_: The selector preceding the event in the binding used on the Control. +- _callback_: The callback function being bound. +- _control_: The Control the event is bound on. + +Inside your processor function, you should bind _callback_ to the event, and +return a function for can.Control to call when _callback_ needs to be unbound. +(If _selector_ is defined, you will likely want to use some form of delegation +to bind the event.) + +Here is a Control with a custom event processor set and two callbacks bound +to that event: + + can.Control.processors.birthday = function(el, ev, selector, callback, control) { + if(selector) { + myFramework.delegate(ev, el, selector, callback); + return function() { myFramework.undelegate(ev, el, selector, callback); }; + } else { + myFramework.bind(ev, el, callback); + return function() { myFramework.unbind(ev, el, callback); }; + } + }; + + can.Control("EventTarget", { }, { + 'birthday': function(el, ev) { + // do something appropriate for the occasion + }, + '.grandchild birthday': function(el, ev) { + // do something appropriate for the occasion + } + }); + + var target = new EventTarget('#person'); + +When `target` is initialized, can.Control will call `can.Control.processors.birthday` +twice (because there are two event hookups for the _birthday_ event). The first +time it's called, the arguments will be: + +- _el_: A NodeList that wraps the element with id 'person'. +- _ev_: `'birthday'` +- _selector_: `''` +- _callback_: The function assigned to `' birthday'` in the prototype section of `EventTarget`'s +definition. +- _control_: `target` itself. + +The second time, the arguments are slightly different: + +- _el_: A NodeList that wraps the element with id 'person'. +- _ev_: `'birthday'` +- _selector_: `'.grandchild'` +- _callback_: The function assigned to `'.grandchild birthday'` in the prototype section of `EventTarget`'s +definition. +- _control_: `target` itself. + +can.Control already has processors for these events: + +- change +- click +- contextmenu +- dblclick +- focusin +- focusout +- keydown +- keyup +- keypress +- mousedown +- mouseenter +- mouseleave +- mousemove +- mouseout +- mouseover +- mouseup +- reset +- resize +- scroll +- select +- submit diff --git a/control/doc/setup.md b/control/doc/setup.md new file mode 100644 index 00000000000..e413d6ce671 --- /dev/null +++ b/control/doc/setup.md @@ -0,0 +1,45 @@ +@function can.Control.prototype.setup setup +@parent can.Control.prototype +@description Perform pre-initialization logic for control instances and classes. + +@signature `control.setup(element, options)` +@param {HTMLElement|NodeList|String} element The element as passed to the constructor. +@param {Object} [options] option values for the control. These get added to +this.options and merged with [can.Control.static.defaults defaults]. +@return {undefined|Array} return an array if you want to change what init is called with. By +default it is called with the element and options passed to the control. + +@body + +## Lifecycle of `setup` + +Setup, when called, does the following: + +### Sets this.element + +The first parameter passed to new Control( el, options ) is expected to be +an element. This gets converted to a Wrapped NodeList element and set as +[can.Control.prototype.element this.element]. + +### Adds the control's name to the element's className + +Control adds it's plugin name to the element's className for easier +debugging. For example, if your Control is named "Foo.Bar", it adds +"foo_bar" to the className. + +### Saves the control in $.data + +A reference to the control instance is saved in $.data. You can find +instances of "Foo.Bar" like: + + $( '#el' ).data( 'controls' )[ 'foo_bar' ] + +### Merges Options + +Merges the default options with optional user-supplied ones. +Additionally, default values are exposed in the static [can.Control.static.defaults defaults] +so that users can change them. + +### Binds event handlers + +Setup does the event binding described in [can.Control]. \ No newline at end of file diff --git a/list/doc/Map.md b/list/doc/Map.md new file mode 100644 index 00000000000..173e9c9f5cc --- /dev/null +++ b/list/doc/Map.md @@ -0,0 +1,32 @@ +@page can.List.Map Map +@parent can.List.static + +@property {can.Map} can.List.Map + +@description Specify the Map type used to make objects added to this list observable. + +@option {can.Map} When objects are added to a can.List, those objects are converted into can.Map instances. For example: + + var list = new can.List(); + list.push({name: "Justin"}); + + var map = list.attr(0); + map.attr("name") //-> "Justin" + +By changing [can.List.Map], you can specify a different type of Map instance to create. For example: + + var User = can.Map.extend({ + fullName: function(){ + return this.attr("first")+" "+this.attr("last") + } + }); + + User.List = can.List.extend({ + Map: User + }, {}); + + var list = new User.List(); + list.push({first: "Justin", last: "Meyer"}); + + var user = list.attr(0); + user.fullName() //-> "Justin Meyer" \ No newline at end of file diff --git a/list/doc/extend.md b/list/doc/extend.md new file mode 100644 index 00000000000..e19cce2e8f2 --- /dev/null +++ b/list/doc/extend.md @@ -0,0 +1,14 @@ +@page can.List.extend extend +@parent can.List.static + +@function extend + +@signature `can.List.extend([name,] [staticProperties,] instanceProperties)` + +Creates a new extended constructor function. Learn more at [can.Construct.extend]. + +@param {String} [name] If provided, adds the extened List constructor function to the window at the given name. + +@param {Object} [staticProperties] Properties and methods directly on the constructor function. The most common property to set is [can.List.Map]. + +@param {Object} [instanceProperties] Properties and methods on instances of this list type. \ No newline at end of file diff --git a/list/list.md b/list/doc/list.md similarity index 96% rename from list/list.md rename to list/doc/list.md index 2a0cddf7c43..53161423ef9 100644 --- a/list/list.md +++ b/list/doc/list.md @@ -4,8 +4,12 @@ @test can/list/test.html @parent canjs @release 2.0 -@group can.List.plugins plugins -@link ../docco/list.html docco + +@group can.List.prototype 0 Prototype +@group can.List.static 1 Static +@group can.List.plugins 2 plugins + +@link ../docco/list/list.html docco Use for observable array-like objects. diff --git a/list/doc/prototype.attr.md b/list/doc/prototype.attr.md new file mode 100644 index 00000000000..cd348571eed --- /dev/null +++ b/list/doc/prototype.attr.md @@ -0,0 +1,243 @@ +@page can.List.prototype.attr attr +@parent can.List.prototype + +@description Get or set elements in a List. +@function can.List.prototype.attr attr + +@signature `list.attr()` + +Gets an array of all the elements in this `can.List`. + +@return {Array} An array with all the elements in this List. + +@signature `list.attr(index)` + +Reads an element from this `can.List`. + +@param {Number} index The element to read. +@return {*} The value at _index_. + +@signature `list.attr(index, value)` + +Assigns _value_ to the index _index_ on this `can.List`, expanding the list if necessary. + +@param {Number} index The element to set. +@param {*} value The value to assign at _index_. +@return {can.List} This list, for chaining. + +@signature `list.attr(elements[, replaceCompletely])` + +Merges the members of _elements_ into this List, replacing each from the beginning in order. If _elements_ is longer than the current List, the current List will be expanded. If _elements_ is shorter than the current List, the extra existing members are not affected (unless _replaceCompletely_ is `true`). To remove elements without replacing them, use `[can.Map::removeAttr removeAttr]`. + +@param {Array} elements An array of elements to merge in. + +@param {bool} [replaceCompletely=false] whether to completely replace the elements of List + +If _replaceCompletely_ is `true` and _elements_ is shorter than the List, the existing extra members of the List will be removed. + +@return {can.List} This list, for chaining. + +@body + + +## Use + +`attr` gets or sets elements on the `can.List` it's called on. Here's a tour through how all of its forms work: + + var people = new can.List(['Alex', 'Bill']); + + // set an element: + people.attr(0, 'Adam'); + + // get an element: + people.attr(0); // 'Adam' + people[0]; // 'Adam' + + // get all elements: + people.attr(); // ['Adam', 'Bill'] + + // extend the array: + people.attr(4, 'Charlie'); + people.attr(); // ['Adam', 'Bill', undefined, undefined, 'Charlie'] + + // merge the elements: + people.attr(['Alice', 'Bob', 'Eve']); + people.attr(); // ['Alice', 'Bob', 'Eve', undefined, 'Charlie'] + +## Deep properties + +`attr` can also set and read deep properties. All you have to do is specify the property name as you normally would if you weren't using `attr`. + +@codestart +var people = new can.List([{name: 'Alex'}, {name: 'Bob'}]); + +// set a property: +people.attr('0.name', 'Alice'); + +// get a property: +people.attr('0.name'); // 'Alice' +people[0].attr('name'); // 'Alice' + +// get all properties: +people.attr(); // [{name: 'Alice'}, {name: 'Bob'}] +@codeend + +The discussion of deep properties under `[can.Map.prototype.attr]` may also be enlightening. + +## Events + +`can.List`s emit five types of events in response to changes. They are: + +- the _change_ event fires on every change to a List. +- the _set_ event is fired when an element is set. +- the _add_ event is fired when an element is added to the List. +- the _remove_ event is fired when an element is removed from the List. +- the _length_ event is fired when the length of the List changes. + +### The _change_ event + +The first event that is fired is the _change_ event. The _change_ event is useful +if you want to react to all changes on an List. + +@codestart +var list = new can.List([]); +list.bind('change', function(ev, index, how, newVal, oldVal) { + console.log('Something changed.'); +}); +@codeend + +The parameters of the event handler for the _change_ event are: + +- _ev_ The event object. +- _index_ Where the change took place. +- _how_ Whether elements were added, removed, or set. + Possible values are `'add'`, `'remove'`, or `'set'`. +- _newVal_ The elements affected after the change + _newVal_ will be a single value when an index is set, an Array when elements +were added, and `undefined` if elements were removed. +- _oldVal_ The elements affected before the change. +_newVal_ will be a single value when an index is set, an Array when elements +were removed, and `undefined` if elements were added. + +Here is a concrete tour through the _change_ event handler's arguments: + +@codestart +var list = new can.List(); +list.bind('change', function(ev, index, how, newVal, oldVal) { + console.log(ev + ', ' + index + ', ' + how + ', ' + newVal + ', ' + oldVal); +}); + +list.attr(['Alexis', 'Bill']); // [object Object], 0, add, ['Alexis', 'Bill'], undefined +list.attr(2, 'Eve'); // [object Object], 2, add, Eve, undefined +list.attr(0, 'Adam'); // [object Object], 0, set, Adam, Alexis +list.attr(['Alice', 'Bob']); // [object Object], 0, set, Alice, Adam + // [object Object], 1, set, Bob, Bill +list.removeAttr(1); // [object Object], 1, remove, undefined, Bob +@codeend + +### The _set_ event + +_set_ events are fired when an element at an index that already exists in the List is modified. Actions can cause _set_ events to fire never also cause _length_ events to fire (although some functions, such as `[can.List.prototype.splice splice]` may cause unrelated sets of events to fire after being batched). + +The parameters of the event handler for the _set_ event are: + +- _ev_ The event object. +- _newVal_ The new value of the element. +- _index_ where the set took place. + +Here is a concrete tour through the _set_ event handler's arguments: + +@codestart +var list = new can.List(); +list.bind('set', function(ev, newVal, index) { + console.log(newVal + ', ' + index); +}); + +list.attr(['Alexis', 'Bill']); +list.attr(2, 'Eve'); +list.attr(0, 'Adam'); // Adam, 0 +list.attr(['Alice', 'Bob']); // Alice, 0 + // Bob, 1 +list.removeAttr(1); +@codeend + +### The _add_ event + +_add_ events are fired when elements are added or inserted +into the List. + +The parameters of the event handler for the _add_ event are: + +- _ev_ The event object. +- _newElements_ The new elements. + If more than one element is added, _newElements_ will be an array. Otherwise, it is simply the new element itself. +- _index_ Where the add or insert took place. + +Here is a concrete tour through the _add_ event handler's arguments: + +@codestart +var list = new can.List(); +list.bind('add', function(ev, newElements, index) { + console.log(newElements + ', ' + index); +}); + +list.attr(['Alexis', 'Bill']); // ['Alexis', 'Bill'], 0 +list.attr(2, 'Eve'); // Eve, 2 +list.attr(0, 'Adam'); +list.attr(['Alice', 'Bob']); + +list.removeAttr(1); +@codeend + +### The _remove_ event + +_remove_ events are fired when elements are removed from the list. + +The parameters of the event handler for the _remove_ event are: + +- _ev_ The event object. +- _removedElements_ The removed elements. + If more than one element was removed, _removedElements_ will be an array. Otherwise, it is simply the element itself. +- _index_ Where the removal took place. + +Here is a concrete tour through the _remove_ event handler's arguments: + +@codestart +var list = new can.List(); +list.bind('remove', function(ev, removedElements, index) { + console.log(removedElements + ', ' + index); +}); + +list.attr(['Alexis', 'Bill']); +list.attr(2, 'Eve'); +list.attr(0, 'Adam'); +list.attr(['Alice', 'Bob']); + +list.removeAttr(1); // Bob, 1 +@codeend + +### The _length_ event + +_length_ events are fired whenever the list changes. + +The parameters of the event handler for the _length_ event are: + +- _ev_ The event object. +- _length_ The current length of the list. + If events were batched when the _length_ event was triggered, _length_ will have the length of the list when `stopBatch` was called. Because of this, you may recieve multiple _length_ events with the same _length_ parameter. + +Here is a concrete tour through the _length_ event handler's arguments: + +@codestart +var list = new can.List(); +list.bind('length', function(ev, length) { + console.log(length); +}); + +list.attr(['Alexis', 'Bill']); // 2 +list.attr(2, 'Eve'); // 3 +list.attr(0, 'Adam'); +list.attr(['Alice', 'Bob']); + +list.removeAttr(1); // 2 +@codeend \ No newline at end of file diff --git a/list/doc/prototype.each.md b/list/doc/prototype.each.md new file mode 100644 index 00000000000..b7b7d0d7c9e --- /dev/null +++ b/list/doc/prototype.each.md @@ -0,0 +1,36 @@ +@page can.List.prototype.each each +@parent can.List.prototype + +@function can.List.prototype.each each +@description Call a function on each element of a List. +@signature `list.each( callback(item, index) )` + +`each` iterates through the Map, calling a function +for each element. + +@param {function(*, Number)} callback the function to call for each element +The value and index of each element will be passed as the first and second +arguments, respectively, to the callback. If the callback returns false, +the loop will stop. + +@return {can.List} this List, for chaining + +@body +@codestart +var i = 0; +new can.Map([1, 10, 100]).each(function(element, index) { + i += element; +}); + +i; // 111 + +i = 0; +new can.Map([1, 10, 100]).each(function(element, index) { + i += element; + if(index >= 1) { + return false; + } +}); + +i; // 11 +@codeend \ No newline at end of file diff --git a/list/doc/prototype.reverse.md b/list/doc/prototype.reverse.md new file mode 100644 index 00000000000..80ab2c8074c --- /dev/null +++ b/list/doc/prototype.reverse.md @@ -0,0 +1,25 @@ +@page can.List.prototype.reverse reverse +@parent can.List.prototype + +@function can.List.prototype.reverse reverse +@description Reverse the order of a List. +@signature `list.reverse()` + +`reverse` reverses the elements of the List in place. + +@return {can.List} the List, for chaining + +@body +@codestart +var list = new can.List(['Alice', 'Bob', 'Eve']); +var reversedList = list.reverse(); + +reversedList.attr(); // ['Eve', 'Bob', 'Alice']; +list === reversedList; // true +@codeend + +`reverse` calls `replace` internally and triggers corresponding `add`, `remove`, `change` and `length` events respectively. + +## Demo + +@iframe can/list/doc/reverse.html 350 \ No newline at end of file diff --git a/list/doc/prototype.splice.md b/list/doc/prototype.splice.md new file mode 100644 index 00000000000..f9885d6d04b --- /dev/null +++ b/list/doc/prototype.splice.md @@ -0,0 +1,65 @@ +@page can.List.prototype.splice splice +@parent can.List.prototype + +@function can.List.prototype.splice splice +@description Insert and remove elements from a List. +@signature `list.splice(index[, howMany[, ...newElements]])` +@param {Number} index where to start removing or inserting elements + +@param {Number} [howMany] the number of elements to remove + If _howMany_ is not provided, `splice` will all elements from `index` to the end of the List. + +@param {*} newElements elements to insert into the List + +@return {Array} the elements removed by `splice` + +@body + `splice` lets you remove elements from and insert elements into a List. + + This example demonstrates how to do surgery on a list of numbers: + +@codestart + var list = new can.List([0, 1, 2, 3]); + + // starting at index 2, remove one element and insert 'Alice' and 'Bob': + list.splice(2, 1, 'Alice', 'Bob'); + list.attr(); // [0, 1, 'Alice', 'Bob', 3] +@codeend + + ## Events + + `splice` causes the List it's called on to emit _change_ events, + _add_ events, _remove_ events, and _length_ events. If there are + any elements to remove, a _change_ event, a _remove_ event, and a + _length_ event will be fired. If there are any elements to insert, a + separate _change_ event, an _add_ event, and a separate _length_ event + will be fired. + + This slightly-modified version of the above example should help + make it clear how `splice` causes events to be emitted: + +@codestart + var list = new can.List(['a', 'b', 'c', 'd']); + list.bind('change', function(ev, attr, how, newVals, oldVals) { + console.log('change: ' + attr + ', ' + how + ', ' + newVals + ', ' + oldVals); + }); + list.bind('add', function(ev, newVals, where) { + console.log('add: ' + newVals + ', ' + where); + }); + list.bind('remove', function(ev, oldVals, where) { + console.log('remove: ' + oldVals + ', ' + where); + }); + list.bind('length', function(ev, length) { + console.log('length: ' + length + ', ' + this.attr()); + }); + + // starting at index 2, remove one element and insert 'Alice' and 'Bob': + list.splice(2, 1, 'Alice', 'Bob'); // change: 2, 'remove', undefined, ['c'] + // remove: ['c'], 2 + // length: 5, ['a', 'b', 'Alice', 'Bob', 'd'] + // change: 2, 'add', ['Alice', 'Bob'], ['c'] + // add: ['Alice', 'Bob'], 2 + // length: 5, ['a', 'b', 'Alice', 'Bob', 'd'] +@codeend + + More information about binding to these events can be found under [can.List.attr attr]. \ No newline at end of file diff --git a/list/doc/reverse.html b/list/doc/reverse.html new file mode 100644 index 00000000000..2aba99f770f --- /dev/null +++ b/list/doc/reverse.html @@ -0,0 +1,55 @@ + + + + + + + + + \ No newline at end of file diff --git a/list/list.js b/list/list.js index 23508eb6e70..d1375603ea5 100644 --- a/list/list.js +++ b/list/list.js @@ -88,6 +88,7 @@ steal("can/util", "can/map", "can/map/bubble.js",function (can, Map, bubble) { this.length = 0; can.cid(this, ".map"); this._init = 1; + this._setupComputes(); instances = instances || []; var teardownMapping; @@ -127,7 +128,15 @@ steal("can/util", "can/map", "can/map/bubble.js",function (can, Map, bubble) { }, __get: function (attr) { - return attr ? this[attr] : this; + if (attr) { + if (this[attr] && this[attr].isComputed && can.isFunction(this.constructor.prototype[attr])) { + return this[attr](); + } else { + return this[attr]; + } + } else { + return this; + } }, ___set: function (attr, val) { this[attr] = val; @@ -871,7 +880,10 @@ steal("can/util", "can/map", "can/map/bubble.js",function (can, Map, bubble) { * list === reversedList; // true * @codeend */ - reverse: [].reverse, + reverse: function() { + var list = can.makeArray([].reverse.call(this)); + this.replace(list); + }, /** * @function can.List.prototype.slice slice diff --git a/list/list_test.js b/list/list_test.js index 92e797ce114..5c88f2b0dc0 100644 --- a/list/list_test.js +++ b/list/list_test.js @@ -1,4 +1,4 @@ -steal("can/util", "can/list", "can/test", function () { +steal("can/util", "can/list", "can/test", "can/compute", function () { module('can/list'); test('list attr changes length', function () { var l = new can.List([ @@ -166,4 +166,34 @@ steal("can/util", "can/list", "can/test", function () { l.splice(0, 1); ok(!l.attr(0), 'all props are removed'); }); + + test('list sets up computed attributes (#790)', function() { + var List = can.List.extend({ + i: can.compute(0), + a: 0 + }); + + var l = new List([1]); + equal(l.attr('i'), 0); + + var Map = can.Map.extend({ + f: can.compute(0) + }); + + var m = new Map(); + m.attr('f'); + }); + + test('reverse triggers add/remove events (#851)', function() { + expect(6); + var l = new can.List([1,2,3]); + + l.bind('change', function() { ok(true, 'change should be called'); }); + l.bind('set', function() { ok(false, 'set should not be called'); }); + l.bind('add', function() { ok(true, 'add called'); }); + l.bind('remove', function() { ok(true, 'remove called'); }); + l.bind('length', function() { ok(true, 'length should be called'); }); + + l.reverse(); + }); }); diff --git a/map/attributes/attributes.js b/map/attributes/attributes.js index c9277442198..536ca9e5ac1 100644 --- a/map/attributes/attributes.js +++ b/map/attributes/attributes.js @@ -11,152 +11,9 @@ steal('can/util', 'can/map', 'can/list', function (can, Map) { return typeof obj === 'object' && obj !== null && obj; }; can.extend(clss, { - /** - * @property can.Map.attributes.static.attributes attributes - * @parent can.Map.attributes.static - * - * `can.Map.attributes` is a property that contains key/value pair(s) of an attribute's name and its - * respective type for using in [can.Map.attributes.static.convert convert] and [can.Map.prototype.serialize serialize]. - * - * var Contact = can.Map.extend({ - * attributes : { - * birthday : 'date', - * age: 'number', - * name: 'string' - * } - * }); - * - */ + attributes: {}, - /** - * @property can.Map.attributes.static.convert convert - * @parent can.Map.attributes.static - * - * You often want to convert from what the observe sends you to a form more useful to JavaScript. - * For example, contacts might be returned from the server with dates that look like: "1982-10-20". - * We can observe to convert it to something closer to `new Date(1982,10,20)`. - * - * Convert comes with the following types: - * - * - __date__ Converts to a JS date. Accepts integers or strings that work with Date.parse - * - __number__ An integer or number that can be passed to parseFloat - * - __boolean__ Converts "false" to false, and puts everything else through Boolean() - * - * The following sets the birthday attribute to "date" and provides a date conversion function: - * - * var Contact = can.Map.extend({ - * attributes : { - * birthday : 'date' - * }, - * convert : { - * date : function(raw){ - * if(typeof raw == 'string'){ - * //- Extracts dates formated 'YYYY-DD-MM' - * var matches = raw.match(/(\d+)-(\d+)-(\d+)/); - * - * //- Parses to date object and returns - * return new Date(matches[1], - * (+matches[2])-1, - * matches[3]); - * - * }else if(raw instanceof Date){ - * return raw; - * } - * } - * } - * },{}); - * - * var contact = new Contact(); - * - * //- calls convert on attribute set - * contact.attr('birthday', '4-26-2012') - * - * contact.attr('birthday'); //-> Date - * - * If a property is set with an object as a value, the corresponding converter is called with the unmerged data (the raw object) - * as the first argument, and the old value (a can.Map) as the second: - * - * var MyObserve = can.Map.extend({ - * attributes: { - * nested: "nested" - * }, - * convert: { - * nested: function(data, oldVal) { - * if(oldVal instanceof MyObserve) { - * return oldVal.attr(data); - * } - * return new MyObserve(data); - * } - * } - * },{}); - * - * ## Differences From `attr` - * - * The way that return values from convertors affect the value of an Observe's property is - * different from [can.Map::attr attr]'s normal behavior. Specifically, when the - * property's current value is an Observe or List, and an Observe or List is returned - * from a convertor, the effect will not be to merge the values into the current value as - * if the return value was fed straight into `attr`, but to replace the value with the - * new Observe or List completely. Because of this, any bindings you have on the previous - * observable object will break. - * - * If you would rather have the new Observe or List merged into the current value, call - * `attr` directly on the property instead of on the Observe: - * - * @codestart - * var Contact = can.Map.extend({ - * attributes: { - * info: 'info' - * }, - * convert: { - * 'info': function(data, oldVal) { - * return data; - * } - * } - * }, {}); - * - * var alice = new Contact({info: {name: 'Alice Liddell', email: 'alice@liddell.com'}}); - * alice.attr(); // {name: 'Alice Liddell', 'email': 'alice@liddell.com'} - * alice.info._cid; // '.observe1' - * - * alice.attr('info', {name: 'Allison Wonderland', phone: '888-888-8888'}); - * alice.attr(); // {name: 'Allison Wonderland', 'phone': '888-888-8888'} - * alice.info._cid; // '.observe2' - * - * alice.info.attr({email: 'alice@wonderland.com', phone: '000-000-0000'}); - * alice.attr(); // {name: 'Allison Wonderland', email: 'alice@wonderland.com', 'phone': '000-000-0000'} - * alice.info._cid; // '.observe2' - * @codeend - * - * ## Assocations and Convert - * - * If you have assocations defined within your model(s), you can use convert to automatically - * call serialize on those models. - * - * @codestart - * var Contact = can.Model.extend({ - * attributes : { - * tasks: Task - * } - * }, {}); - * - * var Task = can.Model.extend({ - * attributes : { - * due : 'date' - * } - * },{}); - * - * var contact = new Contact({ - * tasks: [ new Task({ - * due: new Date() - * }) ] - * }); - * - * contact.serialize(); - * //-> { tasks: [ { due: 1333219754627 } ] } - * @codeend - */ convert: { 'date': function (str) { var type = typeof str; @@ -205,41 +62,6 @@ steal('can/util', 'can/map', 'can/list', function (can, Map) { return typeof construct === 'function' ? construct.call(context, val, oldVal) : val; } }, - /** - * @property can.Map.attributes.static.serialize serialize - * @parent can.Map.attributes.static - * - * `can.Map.serialize` is an object of name-function pairs that are used to - * serialize attributes. - * - * Similar to [can.Map.attributes.static.convert can.Map.attributes.convert], in that the keys of this object correspond to - * the types specified in [can.Map.attributes]. - * - * By default every attribute will be passed through the 'default' serialization method - * that will return the value if the property holds a primitive value (string, number, ...), - * or it will call the "serialize" method if the property holds an object with the "serialize" method set. - * - * For example, to serialize all dates to ISO format: - * - * @codestart - * var Contact = can.Map.extend({ - * attributes : { - * birthday : 'date' - * }, - * serialize : { - * date : function(val, type){ - * return new Date(val).toISOString(); - * } - * } - * },{}); - * - * var contact = new Contact({ - * birthday: new Date("Oct 25, 1973") - * }).serialize(); - * //-> { "birthday" : "1973-10-25T05:00:00.000Z" } - * @codeend - * - */ serialize: { 'default': function (val, type) { return isObject(val) && val.serialize ? val.serialize() : val; @@ -295,44 +117,6 @@ steal('can/util', 'can/map', 'can/list', function (can, Map) { } return value === null || !type ? value : converter.call(Class, value, oldVal, function () {}, type); }; - /** - * @function can.Map.prototype.attributes.serialize serialize - * @parent can.Map.attributes.prototype - * - * @description Serializes the observe's properties using - * the [can.Map.attributes attribute plugin]. - * - * @signature `observe.serialize([attrName])` - * @param {String} [attrName] If passed, returns only a serialization of the named attribute. - * @return {String} A serialization of this Observe. - * - * @body - * You can set the serialization methods similar to the convert methods: - * - * var Contact = can.Map.extend({ - * attributes : { - * birthday : 'date' - * }, - * serialize : { - * date : function( val, type ){ - * return val.getYear() + - * "-" + (val.getMonth() + 1) + - * "-" + val.getDate(); - * } - * } - * },{}) - * - * var contact = new Contact(); - * contact.attr('birthday', new Date()); - * contact.serialize() - * //-> { birthday: 'YYYY-MM-DD' } - * - * You can also get and serialize an individual property by passing the attribute - * name to the `serialize` function. Building on the above demo, we can serialize - * the `birthday` attribute only. - * - * contact.serialize('birthday') //-> 'YYYY-MM-DD' - */ can.List.prototype.serialize = function (attrName, stack) { return can.makeArray(can.Map.prototype.serialize.apply(this, arguments)); }; diff --git a/map/attributes/attributes-assocations.html b/map/attributes/doc/attributes-assocations.html similarity index 96% rename from map/attributes/attributes-assocations.html rename to map/attributes/doc/attributes-assocations.html index 0c925b69137..b3d1323a90d 100644 --- a/map/attributes/attributes-assocations.html +++ b/map/attributes/doc/attributes-assocations.html @@ -14,11 +14,11 @@
- + + diff --git a/route/route.md b/route/route.md index cac3d730ad0..3b7513d9344 100644 --- a/route/route.md +++ b/route/route.md @@ -5,7 +5,7 @@ @test can/route/test.html @parent canjs @group can.route.plugins plugins -@link ../docco/route.html docco +@link ../docco/route/route.html docco @description Manage browser history and client state by synchronizing the window.location.hash with diff --git a/test/compatibility/dojo.html b/test/compatibility/dojo.html index ab8b4a23515..770ad6a0958 100644 --- a/test/compatibility/dojo.html +++ b/test/compatibility/dojo.html @@ -59,6 +59,7 @@

diff --git a/test/compatibility/jquery-2.html b/test/compatibility/jquery-2.html index 7e51215f881..8130c9a5911 100644 --- a/test/compatibility/jquery-2.html +++ b/test/compatibility/jquery-2.html @@ -61,6 +61,7 @@

diff --git a/test/compatibility/jquery.html b/test/compatibility/jquery.html index d123ec04175..62e527a97fd 100644 --- a/test/compatibility/jquery.html +++ b/test/compatibility/jquery.html @@ -61,6 +61,7 @@

diff --git a/test/compatibility/mootools.html b/test/compatibility/mootools.html index d4f68b08ac4..428db048eb4 100644 --- a/test/compatibility/mootools.html +++ b/test/compatibility/mootools.html @@ -59,6 +59,7 @@

diff --git a/test/compatibility/yui.html b/test/compatibility/yui.html index da8524844a2..1903f190690 100644 --- a/test/compatibility/yui.html +++ b/test/compatibility/yui.html @@ -59,6 +59,7 @@

diff --git a/test/compatibility/zepto.html b/test/compatibility/zepto.html index 9cba278b843..91eae61d292 100644 --- a/test/compatibility/zepto.html +++ b/test/compatibility/zepto.html @@ -59,6 +59,7 @@

diff --git a/test/dev/dojo.html b/test/dev/dojo.html index c0809cf965c..2a1e6ba4f25 100644 --- a/test/dev/dojo.html +++ b/test/dev/dojo.html @@ -61,6 +61,7 @@

diff --git a/test/dev/jquery-2.html b/test/dev/jquery-2.html index 03c16e5d9ce..b3cb5830813 100644 --- a/test/dev/jquery-2.html +++ b/test/dev/jquery-2.html @@ -63,6 +63,7 @@

diff --git a/test/dev/jquery.html b/test/dev/jquery.html index 40bcb0d46b0..b33ac1b2805 100644 --- a/test/dev/jquery.html +++ b/test/dev/jquery.html @@ -63,6 +63,7 @@

diff --git a/test/dev/mootools.html b/test/dev/mootools.html index 2c99e618f51..3bede1ea748 100644 --- a/test/dev/mootools.html +++ b/test/dev/mootools.html @@ -61,6 +61,7 @@

diff --git a/test/dev/yui.html b/test/dev/yui.html index c02cb8ca2e7..883da4e8028 100644 --- a/test/dev/yui.html +++ b/test/dev/yui.html @@ -61,6 +61,7 @@

diff --git a/test/dev/zepto.html b/test/dev/zepto.html index 89f5f0c0d34..516c11e9278 100644 --- a/test/dev/zepto.html +++ b/test/dev/zepto.html @@ -61,6 +61,7 @@

diff --git a/test/dist/dojo.html b/test/dist/dojo.html index 6badf1fd963..0358b3a6f7d 100644 --- a/test/dist/dojo.html +++ b/test/dist/dojo.html @@ -59,6 +59,7 @@

diff --git a/test/dist/jquery-2.html b/test/dist/jquery-2.html index 08d2796d7ab..7d415bf738b 100644 --- a/test/dist/jquery-2.html +++ b/test/dist/jquery-2.html @@ -61,6 +61,7 @@

diff --git a/test/dist/jquery.html b/test/dist/jquery.html index db1ccf4b909..5da7ef94fc4 100644 --- a/test/dist/jquery.html +++ b/test/dist/jquery.html @@ -61,6 +61,7 @@

diff --git a/test/dist/mootools.html b/test/dist/mootools.html index 6aa05f2f9b6..8bd3e33fa00 100644 --- a/test/dist/mootools.html +++ b/test/dist/mootools.html @@ -59,6 +59,7 @@

diff --git a/test/dist/yui.html b/test/dist/yui.html index a742fbe4cc0..11e73dc93ea 100644 --- a/test/dist/yui.html +++ b/test/dist/yui.html @@ -59,6 +59,7 @@

diff --git a/test/dist/zepto.html b/test/dist/zepto.html index 5731fd5be7f..708d9761a57 100644 --- a/test/dist/zepto.html +++ b/test/dist/zepto.html @@ -59,6 +59,7 @@

diff --git a/test/pluginified/2.0.5.test.js b/test/pluginified/2.0.5.test.js index a4f584013d6..e36e649b9ea 100644 --- a/test/pluginified/2.0.5.test.js +++ b/test/pluginified/2.0.5.test.js @@ -6402,7 +6402,7 @@ var __m42 = (function () { '{noExistStuff} proc': function () {} }); var c = new Control(document.createElement('div')); - equal(c._bindings.length, 1, 'There is only one binding'); + equal(c._bindings.user.length, 1, 'There is only one binding'); }); test('Multiple calls to destroy', 2, function () { var Control = can.Control({ diff --git a/test/templates/__configuration__-compat.html.ejs b/test/templates/__configuration__-compat.html.ejs index 557dc45e8d1..4e7e713c2e0 100644 --- a/test/templates/__configuration__-compat.html.ejs +++ b/test/templates/__configuration__-compat.html.ejs @@ -42,6 +42,7 @@ <% }); %> diff --git a/test/templates/__configuration__-dev.html.ejs b/test/templates/__configuration__-dev.html.ejs index 8c5ac5b0cc6..8d7a67c63ab 100644 --- a/test/templates/__configuration__-dev.html.ejs +++ b/test/templates/__configuration__-dev.html.ejs @@ -42,6 +42,7 @@ diff --git a/test/templates/__configuration__-dist.html.ejs b/test/templates/__configuration__-dist.html.ejs index e751b221478..c9b2ca92fa4 100644 --- a/test/templates/__configuration__-dist.html.ejs +++ b/test/templates/__configuration__-dist.html.ejs @@ -40,6 +40,7 @@ diff --git a/util/attr/attr.js b/util/attr/attr.js index 65142f94b0c..8d3bba4758a 100644 --- a/util/attr/attr.js +++ b/util/attr/attr.js @@ -1,19 +1,25 @@ -steal("can/util/can.js", function (can) { +// # can/util/attr.js +// Central location for attribute changing to occur, used to trigger an +// `attributes` event on elements. This enables the user to do (jQuery example): `$(el).bind("attributes", function(ev) { ... })` where `ev` contains `attributeName` and `oldValue`. + - // # can/util/attr - // Contains helpers for dealing with element attributes. +steal("can/util/can.js", function (can) { + // Acts as a polyfill for setImmediate which only works in IE 10+. Needed to make + // the triggering of `attributes` event async. var setImmediate = window.setImmediate || function (cb) { return setTimeout(cb, 0); }, attr = { - // Keep a reference to MutationObserver because we need to trigger - // events for browsers that do not support it. + // This property lets us know if the browser supports mutation observers. + // If they are supported then that will be setup in can/util/jquery and those native events will be used to inform observers of attribute changes. + // Otherwise this module handles triggering an `attributes` event on the element. MutationObserver: window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, /** * @property {Object.} can.view.attr.map * @parent can.view.elements + * @hide * * * A mapping of @@ -40,7 +46,8 @@ steal("can/util/can.js", function (can) { "disabled": true, "readonly": true, "required": true, - // setter function for the src attribute + // For the `src` attribute we are using a setter function to prevent values such as an empty string or null from being set. + // An `img` tag attempts to fetch the `src` when it is set, so we need to prevent that from happening by removing the attribute instead. src: function (el, val) { if (val == null || val === "") { el.removeAttribute("src"); @@ -50,19 +57,18 @@ steal("can/util/can.js", function (can) { return val; } }, - // setter function for a style attribute style: function (el, val) { return el.style.cssText = val || ""; } }, - // Elements whos default value we should set + // These are elements whos default value we should set. defaultValue: ["input", "textarea"], // ## attr.set - // Set the value an attribute on an element + // Set the value an attribute on an element. set: function (el, attrName, val) { var oldValue; + // In order to later trigger an event we need to compare the new value to the old value, so here we go ahead and retrieve the old value for browsers that don't have native MutationObservers. if (!attr.MutationObserver) { - // Get the current value oldValue = attr.get(el, attrName); } @@ -70,7 +76,10 @@ steal("can/util/can.js", function (can) { .toLowerCase(), prop = attr.map[attrName], newValue; - // if this is a special property call the setter + + // Using the property of `attr.map`, go through and check if the property is a function, and if so call it. Then check if the property is `true`, and if so set the value to `true`, also making sure to set `defaultChecked` to `true` for elements of `attr.defaultValue`. We always set the value to true because for these boolean properties, setting them to false would be the same as removing the attribute. + // + // For all other attributes use `setAttribute` to set the new value. if (typeof prop === "function") { newValue = prop(el, val); } else if (prop === true) { @@ -83,7 +92,6 @@ steal("can/util/can.js", function (can) { } } else if (prop) { - // set the value as true / false newValue = el[prop] = val; if (prop === "value" && can.inArray(tagName, attr.defaultValue) >= 0) { el.defaultValue = val; @@ -92,16 +100,16 @@ steal("can/util/can.js", function (can) { el.setAttribute(attrName, val); newValue = val; } + + // Now that the value has been set, for browsers without MutationObservers, check to see that value has changed and if so trigger the "attributes" event on the element. if (!attr.MutationObserver && newValue !== oldValue) { attr.trigger(el, attrName, oldValue); } }, // ## attr.trigger - // Trigger an "attributes" event on an element + // Used to trigger an "attributes" event on an element. Checks to make sure that someone is listening for the event and then queues a function to be called asynchronously using `setImmediate. trigger: function (el, attrName, oldValue) { - // Only trigger if someone has bound if (can.data(can.$(el), "canHasAttributesBindings")) { - // Queue up a function to be called return setImmediate(function () { can.trigger(el, { type: "attributes", @@ -114,17 +122,19 @@ steal("can/util/can.js", function (can) { } }, // ## attr.get - // Gets the value of an attribute. + // Gets the value of an attribute. First checks to see if the property is a string on `attr.map` and if so returns the value from the element's property. Otherwise uses `getAttribute` to retrieve the value. get: function (el, attrName) { - // Default to a blank string for IE7/8 - // Try to get the attribute from the element before - // using `getAttribute` - return (attr.map[attrName] && el[attr.map[attrName]] ? - el[attr.map[attrName]] : - el.getAttribute(attrName)); + var prop = attr.map[attrName]; + if(typeof prop === "string" && el[prop]) { + return el[prop]; + } + + return el.getAttribute(attrName); }, // ## attr.remove - // Removes the attribute. + // Removes an attribute from an element. Works by using the `attr.map` to see if the attribute is a special type of property. If the property is a function then the fuction is called with `undefined` as the value. If the property is `true` then the attribute is set to false. If the property is a string then the attribute is set to an empty string. Otherwise `removeAttribute` is used. + // + // If the attribute previously had a value and the browser doesn't support MutationObservers we then trigger an "attributes" event. remove: function (el, attrName) { var oldValue; if (!attr.MutationObserver) { @@ -132,7 +142,6 @@ steal("can/util/can.js", function (can) { } var setter = attr.map[attrName]; - // A special type of attribute, call the function if (typeof setter === "function") { setter(el, undefined); } @@ -144,15 +153,14 @@ steal("can/util/can.js", function (can) { el.removeAttribute(attrName); } if (!attr.MutationObserver && oldValue != null) { - // Trigger that the attribute has changed attr.trigger(el, attrName, oldValue); } }, // ## attr.has + // Checks if an element contains an attribute. + // For browsers that support `hasAttribute`, creates a function that calls hasAttribute, otherwise creates a function that uses `getAttribute` to check that the attribute is not null. has: (function () { - // Use hasAttribute if the browser supports it, - // otherwise check that the attribute's value is not null var el = document.createElement('div'); if (el.hasAttribute) { return function (el, name) { diff --git a/util/events/attributes.md b/util/events/attributes.md index c20ad4fe0bf..63b01d1ea8f 100644 --- a/util/events/attributes.md +++ b/util/events/attributes.md @@ -36,10 +36,12 @@ Listen to an `attributes` event with [can.Control] like: Listen to an `attributes` event with [can.Component::events can.Component's events] object like: can.Component.extend({ - tag: "panel" - "attributes": function(el, ev){ - - } + tag: "panel", + events: { + "attributes": function(el, ev){ + + } + } }) diff --git a/util/fixture/doc/create.md b/util/fixture/doc/create.md new file mode 100644 index 00000000000..5d099190422 --- /dev/null +++ b/util/fixture/doc/create.md @@ -0,0 +1,15 @@ +@description Simulate creating a Model with a fixture. +@function can.fixture.types.Store.create +@parent can.fixture.types.Store +@signature `store.create(request, callback)` +@param {Object} request Parameters for the request. +@param {Function} callback A function to call with the created item. + +@body +`store.destroy(request, callback)` simulates a request to destroy an item from the server. + + + todosStore.create({ + url: "/todos" + }, function(){ }); + diff --git a/util/fixture/doc/delay.md b/util/fixture/doc/delay.md new file mode 100644 index 00000000000..4db86b8fac0 --- /dev/null +++ b/util/fixture/doc/delay.md @@ -0,0 +1,12 @@ +@property {Number} can.fixture.delay delay +@parent can.fixture + +`can.fixture.delay` indicates the delay in milliseconds between an ajax request is made and +the success and complete handlers are called. This only sets +functional synchronous fixtures that return a result. By default, the delay is 200ms. + + + steal('can/util/fixtures').then(function(){ + can.fixture.delay = 1000; + }) + diff --git a/util/fixture/doc/destroy.md b/util/fixture/doc/destroy.md new file mode 100644 index 00000000000..e8fe2766be5 --- /dev/null +++ b/util/fixture/doc/destroy.md @@ -0,0 +1,17 @@ +@description Simulate destroying a Model on a fixture. +@function can.fixture.types.Store.destroy +@parent can.fixture.types.Store +@signature `store.destroy(request, callback)` +@param {Object} request Parameters for the request. +@param {Function} callback A function to call after destruction. + +@body +`store.destroy(request, response())` simulates a request to destroy an item from the server. + + + todosStore.destroy({ + url: "/todos/5" + }, function(){ + + }); + diff --git a/util/fixture/doc/find.md b/util/fixture/doc/find.md new file mode 100644 index 00000000000..b5a0f2948b3 --- /dev/null +++ b/util/fixture/doc/find.md @@ -0,0 +1,15 @@ +@description Get an item from the store by ID. +@function can.fixture.types.Store.find +@parent can.fixture.types.Store +@signature `store.find(settings)` +@param {Object} settings An object containing an `id` key corresponding to the item to find. + +@body +`store.find(settings)` +`store.destroy(request, callback)` simulates a request to get a single item from the server. + + + todosStore.find({ + url: "/todos/5" + }, function(){ }); + diff --git a/util/fixture/doc/findAll.md b/util/fixture/doc/findAll.md new file mode 100644 index 00000000000..d7a928f818f --- /dev/null +++ b/util/fixture/doc/findAll.md @@ -0,0 +1,44 @@ +@description Simulate a findAll to a fixture. +@function can.fixture.types.Store.findAll +@parent can.fixture.types.Store +@signature `store.findAll(request)` + +`store.findAll(request)` simulates a request to +get a list items from the server. It supports the +following params: + +- order - `order=name ASC` +- group - `group=name` +- limit - `limit=20` +- offset - `offset=60` +- id filtering - `ownerId=5` + + +@param {{}} request The ajax request object. The available parameters are: +@option {String} order The order of the results. +`order: 'name ASC'` +@option {String} group How to group the results. +`group: 'name'` +@option {String} limit A limit on the number to retrieve. +`limit: 20` +@option {String} offset The offset of the results. +`offset: 60` +@option {String} id Filtering by ID. +`id: 5` + +@return {Object} a response object like: + + { + count: 1000, + limit: 20, + offset: 60, + data: [item1, item2, ...] + } + +where: + +- count - the number of items that match any filtering before limit and offset is taken into account +- offset - the offset passed +- limit - the limit passed +- data - an array of JS objects with each item's properties + diff --git a/util/fixture/doc/findOne.md b/util/fixture/doc/findOne.md new file mode 100644 index 00000000000..ca0b5a72684 --- /dev/null +++ b/util/fixture/doc/findOne.md @@ -0,0 +1,17 @@ +@description Simulate a findOne request on a fixture. +@function can.fixture.types.Store.findOne +@parent can.fixture.types.Store +@signature `store.findOne(request, callback)` +@param {Object} request Parameters for the request. +@param {Function} callback A function to call with the retrieved item. + +@body +`store.findOne(request, response(item))` simulates a request to +get a single item from the server by id. + + todosStore.findOne({ + url: "/todos/5" + }, function(todo){ + + }); + diff --git a/util/fixture/fixture.md b/util/fixture/doc/fixture.md similarity index 100% rename from util/fixture/fixture.md rename to util/fixture/doc/fixture.md diff --git a/util/fixture/doc/on.md b/util/fixture/doc/on.md new file mode 100644 index 00000000000..08fe52ecf41 --- /dev/null +++ b/util/fixture/doc/on.md @@ -0,0 +1,9 @@ +@property {Boolean} can.fixture.on on +@parent can.fixture + +`can.fixture.on` lets you programatically turn off fixtures. This is mostly used for testing. + + can.fixture.on = false + Task.findAll({}, function(){ + can.fixture.on = true; + }) diff --git a/util/fixture/doc/rand.md b/util/fixture/doc/rand.md new file mode 100644 index 00000000000..d243d455bd1 --- /dev/null +++ b/util/fixture/doc/rand.md @@ -0,0 +1,38 @@ +@description Create a random number or selection. +@function can.fixture.rand rand +@parent can.fixture +@signature `can.fixture.rand([min,] max)` +@param {Number} [min=0] The lower bound on integers to select. +@param {Number} max The upper bound on integers to select. +@return {Number} A random integer in the range [__min__, __max__). + +@signature `can.fixture.rand(choices, min[ ,max])` +@param {Array} choices An array of things to choose from. +@param {Number} min The minimum number of times to choose from __choices__. +@param {Number} [max=min] The maximum number of times to choose from __choices__. +@return {Array} An array of between __min__ and __max__ random choices from __choices__. + +@body +`can.fixture.rand` creates random integers or random arrays of other arrays. + +## Examples + + var rand = can.fixture.rand; + + // get a random integer between 0 and 10 (inclusive) + rand(11); + + // get a random number between -5 and 5 (inclusive) + rand(-5, 6); + + // pick a random item from an array + rand(["j","m","v","c"],1)[0] + + // pick a random number of items from an array + rand(["j","m","v","c"]) + + // pick 2 items from an array + rand(["j","m","v","c"],2) + + // pick between 2 and 3 items at random + rand(["j","m","v","c"],2,3) diff --git a/util/fixture/requestHandler.md b/util/fixture/doc/requestHandler.md similarity index 100% rename from util/fixture/requestHandler.md rename to util/fixture/doc/requestHandler.md diff --git a/util/fixture/doc/reset.md b/util/fixture/doc/reset.md new file mode 100644 index 00000000000..b1d65494a45 --- /dev/null +++ b/util/fixture/doc/reset.md @@ -0,0 +1,18 @@ +@description Reset the fixture store. +@function can.fixture.types.Store.reset +@parent can.fixture.types.Store +@signature `store.reset()` + +@body +`store.reset()` resets the store to contain its original data. This is useful for making tests that operate independently. + +## Basic Example + +After creating a `taskStore` and hooking it up to a `task` model in the "Basic Example" in [can.fixture.store store's docs], a test might create several tasks like: + + new Task({name: "Take out trash", ownerId: 5}).save(); + +But, another test might need to operate on the original set of tasks created by `can.fixture.store`. Reset the task store with: + + taskStore.reset() + diff --git a/util/fixture/responseHandler.md b/util/fixture/doc/responseHandler.md similarity index 100% rename from util/fixture/responseHandler.md rename to util/fixture/doc/responseHandler.md diff --git a/util/fixture/doc/rootUrl.md b/util/fixture/doc/rootUrl.md new file mode 100644 index 00000000000..df10e5849d2 --- /dev/null +++ b/util/fixture/doc/rootUrl.md @@ -0,0 +1,6 @@ +@property {String} can.fixture.rootUrl rootUrl +@parent can.fixture + +`can.fixture.rootUrl` contains the root URL for fixtures to use. +If you are using StealJS it will use the Steal root +URL by default. \ No newline at end of file diff --git a/util/fixture/doc/store.md b/util/fixture/doc/store.md new file mode 100644 index 00000000000..186872a61b4 --- /dev/null +++ b/util/fixture/doc/store.md @@ -0,0 +1,79 @@ +@description Make a store of objects to use when making requests against fixtures. +@function can.fixture.store store +@parent can.fixture + +@signature `can.fixture.store(count, make[, filter])` + +@param {Number} count The number of items to create. + +@param {Function} make A function that will return the JavaScript object. The +make function is called back with the id and the current array of items. + +@param {Function} [filter] A function used to further filter results. Used for to simulate +server params like searchText or startDate. +The function should return true if the item passes the filter, +false otherwise. For example: + + + function(item, settings){ + if(settings.data.searchText){ + var regex = new RegExp("^"+settings.data.searchText) + return regex.test(item.name); + } + } + +@return {can.fixture.Store} A generator object providing fixture functions for *findAll*, *findOne*, *create*, *update* and *destroy*. + +@body +`can.fixture.store(count, generator(index,items))` is used +to create a store of items that can simulate a full CRUD service. Furthermore, +the store can do filtering, grouping, sorting, and paging. + +## Basic Example + +The following creates a store for 100 todos: + + var todoStore = can.fixture.store(100, function(i){ + return { + id: i, + name: "todo number "+i, + description: "a description of some todo", + ownerId: can.fixture.rand(10) + } + }) + +`todoStore`'s methods: + +- [can.fixture.types.Store.findAll findAll], +- [can.fixture.types.Store.findOne findOne], +- [can.fixture.types.Store.create create], +- [can.fixture.types.Store.update update], and +- [can.fixture.types.Store.destroy destroy] + +Can be used to simulate a REST service like: + + can.fixture({ + 'GET /todos': todoStore.findAll, + 'GET /todos/{id}': todoStore.findOne, + 'POST /todos': todoStore.create, + 'PUT /todos/{id}': todoStore.update, + 'DELETE /todos/{id}': todoStore.destroy + }); + +These fixtures, combined with a [can.Model] that connects to these services like: + + var Todo = can.Model.extend({ + findAll : 'GET /todos', + findOne : 'GET /todos/{id}', + create : 'POST /todos', + update : 'PUT /todos/{id}', + destroy : 'DELETE /todos/{id}' + }, {}); + +... allows you to simulate requests for all of owner 5's todos like: + + Todo.findAll({ownerId: 5}, function(todos){ + + }) + + diff --git a/util/fixture/store.md b/util/fixture/doc/storeTypes.md similarity index 100% rename from util/fixture/store.md rename to util/fixture/doc/storeTypes.md diff --git a/util/fixture/doc/update.md b/util/fixture/doc/update.md new file mode 100644 index 00000000000..d60da9652c2 --- /dev/null +++ b/util/fixture/doc/update.md @@ -0,0 +1,16 @@ +@description Simulate an update on a fixture. +@function can.fixture.types.Store.update +@parent can.fixture.types.Store +@signature `store.update(request, callback)` +@param {Object} request Parameters for the request. +@param {Function} callback A function to call with the updated item and headers. + +@body +`store.update(request, response(props,headers))` simulates a request to update an items properties on a server. + + todosStore.update({ + url: "/todos/5" + }, function(props, headers){ + props.id //-> 5 + headers.location // "todos/5" + }); diff --git a/util/fixture/doc/xhr.md b/util/fixture/doc/xhr.md new file mode 100644 index 00000000000..6e436dd8621 --- /dev/null +++ b/util/fixture/doc/xhr.md @@ -0,0 +1,30 @@ +@hide + +Use can.fixture.xhr to create an object that looks like an xhr object. + +## Example + +The following example shows how the -restCreate fixture uses xhr to return a simulated xhr object: + + "-restCreate" : function( settings, cbType ) { + switch(cbType) { + case "success": + return [ + {id: parseInt(Math.random()*1000)}, + "success", + can.fixture.xhr() + ]; + case "complete": + return [ + can.fixture.xhr({ + getResponseHeader: function() { + return settings.url+"/"+parseInt(Math.random()*1000); + } + }), + "success" + ]; + } + } + +@param {Object} [xhr] properties that you want to overwrite +@return {Object} an object that looks like a successful XHR object. diff --git a/util/fixture/fixture.js b/util/fixture/fixture.js index 1c3587a04d9..f0a3d0357f8 100644 --- a/util/fixture/fixture.js +++ b/util/fixture/fixture.js @@ -1,4 +1,10 @@ +// # can/util/fixture.js +// +// Intercepts AJAX requests and simulates them with either a function or a +// file. This is used to develop independently from backend services. steal('can/util', 'can/util/string', 'can/util/object', function (can) { + // can.fixture relies on can.Object in order to work and needs to be + // included before can.fixture in order to use it, otherwise it'll error. if (!can.Object) { throw new Error('can.fixture depends on can.Object. Please include it before can.fixture.'); } @@ -17,12 +23,15 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { return (can.fixture.rootUrl || '') + url; }; + // Manipulates the AJAX prefilter to identify whether or not we should + // manipulate the AJAX call to change the URL to a static file or call + // a function for a dynamic fixture. var updateSettings = function (settings, originalOptions) { if (!can.fixture.on) { return; } - //simple wrapper for logging + // A simple wrapper for logging fixture.js. var log = function () { //!steal-remove-start can.dev.log('can/fixture/fixture.js: ' + Array.prototype.slice.call(arguments) @@ -36,7 +45,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { // add the fixture option if programmed in var data = overwrite(settings); - // if we don't have a fixture, do nothing + // If there is not a fixture for this AJAX request, do nothing. if (!settings.fixture) { if (window.location.protocol === "file:") { log("ajax request to " + settings.url + ", no fixture found"); @@ -44,15 +53,18 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { return; } - //if referencing something else, update the fixture option + // If the fixture already exists on can.fixture, update the fixture option if (typeof settings.fixture === "string" && can.fixture[settings.fixture]) { settings.fixture = can.fixture[settings.fixture]; } - // if a string, we just point to the right url + // If the fixture setting is a string, we just change the URL of the + // AJAX call to the fixture URL. if (typeof settings.fixture === "string") { var url = settings.fixture; + // If the URL starts with //, we need to update the URL to become + // the full path. if (/^\/\//.test(url)) { // this lets us use rootUrl w/o having steal... url = getUrl(settings.fixture.substr(2)); @@ -69,20 +81,25 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { log("looking for fixture in " + url); //!steal-remove-end + // Override the AJAX settings, changing the URL to the fixture file, + // removing the data, and changing the type to GET. settings.url = url; settings.data = null; settings.type = "GET"; if (!settings.error) { + // If no error handling is provided, we provide one and throw an + // error. settings.error = function (xhr, error, message) { throw "fixtures.js Error " + error + " " + message; }; } + // Otherwise, it is a function and we add the fixture data type so the + // fixture transport will handle it. } else { //!steal-remove-start log("using a dynamic fixture for " + settings.type + " " + settings.url); //!steal-remove-end - //it's a function ... add the fixture datatype so our fixture transport handles it // TODO: make everything go here for timing and other fun stuff // add to settings data from fixture ... if (settings.dataTypes) { @@ -90,6 +107,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } if (data && originalOptions) { + originalOptions.data = originalOptions.data || {}; can.extend(originalOptions.data, data); } } @@ -115,8 +133,8 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } return [status, statusText, extractResponses(this, responses), headers]; }, - // If we get data instead of responses, - // make sure we provide a response type that matches the first datatype (typically json) + // If we get data instead of responses, make sure we provide a response + // type that matches the first datatype (typically JSON) extractResponses = function (settings, responses) { var next = settings.dataTypes ? settings.dataTypes[0] : (settings.dataType || 'json'); if (!responses || !responses[next]) { @@ -127,8 +145,11 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { return responses; }; - //used to check urls - // check if jQuery + // Set up prefiltering and transmission handling in order to actually power + // can.fixture. This is handled two different ways, depending on whether or + // not CanJS is using jQuery or not. + + // If we are using jQuery, we have access to ajaxPrefilter and ajaxTransport if (can.ajaxPrefilter && can.ajaxTransport) { // the pre-filter needs to re-route the url @@ -154,7 +175,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { // get the result form the fixture result = s.fixture(original, success, headers, s); if (result !== undefined) { - // make sure the result has the right dataType + // Run the callback as a 200 success and with the results with the correct dataType callback(200, "success", extractResponses(s, result), {}); } }, can.fixture.delay); @@ -165,25 +186,30 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } }; }); + // If we are not using jQuery, we don't have access to those nice ajaxPrefilter + // and ajaxTransport functions, so we need to monkey patch can.ajax. } else { var AJAX = can.ajax; can.ajax = function (settings) { updateSettings(settings, settings); + + // If the call is a fixture call, we run the same type of code as we would + // with jQuery's ajaxTransport. if (settings.fixture) { - var timeout, d = new can.Deferred(), + var timeout, deferred = new can.Deferred(), stopped = false; //TODO this should work with response - d.getResponseHeader = function () {}; + deferred.getResponseHeader = function () {}; - // call success and fail - d.then(settings.success, settings.fail); + // Call success or fail after deferred resolves + deferred.then(settings.success, settings.fail); - // abort should stop the timeout and calling success - d.abort = function () { + // Abort should stop the timeout and calling the success callback + deferred.abort = function () { clearTimeout(timeout); stopped = true; - d.reject(d); + deferred.reject(deferred); }; // set a timeout that simulates making a request .... timeout = setTimeout(function () { @@ -193,29 +219,31 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { status = response[0]; if ((status >= 200 && status < 300 || status === 304) && stopped === false) { - d.resolve(response[2][settings.dataType]); + deferred.resolve(response[2][settings.dataType]); } else { // TODO probably resolve better - d.reject(d, 'error', response[1]); + deferred.reject(deferred, 'error', response[1]); } }, - // get the result form the fixture + // Get the results from the fixture. result = settings.fixture(settings, success, settings.headers, settings); if (result !== undefined) { - d.resolve(result); + // Resolve with fixture results + deferred.resolve(result); } }, can.fixture.delay); - return d; + return deferred; + // Otherwise just run a normal can.ajax call. } else { return AJAX(settings); } }; } - // a list of 'overwrite' settings object + // A list of 'overwrite' settings objects var overwrites = [], - // returns the index of an overwrite function + // Finds and returns the index of an overwrite function find = function (settings, exact) { for (var i = 0; i < overwrites.length; i++) { if ($fixture._similar(settings, overwrites[i], exact)) { @@ -224,7 +252,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } return -1; }, - // overwrites the settings fixture if an overwrite matches + // Overwrites the settings fixture if an overwrite matches overwrite = function (settings) { var index = find(settings); if (index > -1) { @@ -233,7 +261,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } }, - // Makes an attempt to guess where the id is at in the url and returns it. + // Attemps to guess where the id is in an AJAX call's URL and returns it. getId = function (settings) { var id = settings.data.id; @@ -241,55 +269,57 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { id = settings.data; } - /* - Check for id in params(if query string) - If this is just a string representation of an id, parse - if(id === undefined && typeof settings.data === "string") { - id = settings.data; - } - //*/ - + // Parses the URL looking for all digits if (id === undefined) { + // Set id equal to the value settings.url.replace(/\/(\d+)(\/|$|\.)/g, function (all, num) { id = num; }); } if (id === undefined) { + // If that doesn't work Parses the URL looking for all words id = settings.url.replace(/\/(\w+)(\/|$|\.)/g, function (all, num) { + // As long as num isn't the word "update", set id equal to the value if (num !== 'update') { id = num; } }); } - if (id === undefined) { // if still not set, guess a random number + if (id === undefined) { + // If id is still not set, a random number is guessed. id = Math.round(Math.random() * 1000); } return id; }; + // ## can.fixture + // Simulates AJAX requests. var $fixture = can.fixture = function (settings, fixture) { - // if we provide a fixture ... + // If fixture is provided, set up a new fixture. if (fixture !== undefined) { if (typeof settings === 'string') { - // handle url strings + // Match URL if it has GET, POST, PUT, or DELETE. var matches = settings.match(/(GET|POST|PUT|DELETE) (.+)/i); + // If not, we don't set the type, which eventually defaults to GET if (!matches) { settings = { url: settings }; + // If it does match, we split the URL in half and create an object with + // each half as the url and type properties. } else { settings = { url: matches[2], type: matches[1] }; } - } - //handle removing. An exact match if fixture was provided, otherwise, anything similar + // Check if the same fixture was previously added, if so, we remove it + // from our array of fixture overwrites. var index = find(settings, !! fixture); if (index > -1) { overwrites.splice(index, 1); @@ -299,6 +329,9 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } settings.fixture = fixture; overwrites.push(settings); + // If a fixture isn't provided, we assume that settings is + // an array of fixtures, and we should iterate over it, and set up + // the new fixtures. } else { can.each(settings, function (fixture, url) { $fixture(url, fixture); @@ -308,7 +341,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { var replacer = can.replacer; can.extend(can.fixture, { - // given ajax settings, find an overwrite + // Find an overwrite, given some ajax settings. _similar: function (settings, overwrite, exact) { if (exact) { return can.Object.same(settings, overwrite, { @@ -318,6 +351,7 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { return can.Object.subset(settings, overwrite, can.fixture._compare); } }, + // Comparator object used to find a similar overwrite. _compare: { url: function (a, b) { return !!$fixture._getData(b, a); @@ -325,11 +359,16 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { fixture: null, type: "i" }, - // gets data from a url like "/todo/{id}" given "todo/5" + // Returns data from a url, given a fixtue URL. For example, given + // "todo/{id}" and "todo/5", it will return an object with an id property + // equal to 5. _getData: function (fixtureUrl, url) { var order = [], + // Sanitizes fixture URL fixtureUrlAdjusted = fixtureUrl.replace('.', '\\.') .replace('?', '\\?'), + // Creates a regular expression out of the adjusted fixture URL and + // runs it on the URL we passed in. res = new RegExp(fixtureUrlAdjusted.replace(replacer, function (whole, part) { order.push(part); return "([^\/]+)"; @@ -337,97 +376,21 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { .exec(url), data = {}; + // If there were no matches, return null; if (!res) { return null; } + + // Shift off the URL and just keep the data. res.shift(); can.each(order, function (name) { + // Add data from regular expression onto data object. data[name] = res.shift(); }); return data; }, - /** - * @description Make a store of objects to use when making requests against fixtures. - * @function can.fixture.store store - * @parent can.fixture - * - * @signature `can.fixture.store(count, make[, filter])` - * - * @param {Number} count The number of items to create. - * - * @param {Function} make A function that will return the JavaScript object. The - * make function is called back with the id and the current array of items. - * - * @param {Function} [filter] A function used to further filter results. Used for to simulate - * server params like searchText or startDate. - * The function should return true if the item passes the filter, - * false otherwise. For example: - * - * - * function(item, settings){ - * if(settings.data.searchText){ - * var regex = new RegExp("^"+settings.data.searchText) - * return regex.test(item.name); - * } - * } - * - * @return {can.fixture.Store} A generator object providing fixture functions for *findAll*, *findOne*, *create*, - * *update* and *destroy*. - * - * @body - * `can.fixture.store(count, generator(index,items))` is used - * to create a store of items that can simulate a full CRUD service. Furthermore, - * the store can do filtering, grouping, sorting, and paging. - * - * ## Basic Example - * - * The following creates a store for 100 todos: - * - * var todoStore = can.fixture.store(100, function(i){ - * return { - * id: i, - * name: "todo number "+i, - * description: "a description of some todo", - * ownerId: can.fixture.rand(10) - * } - * }) - * - * `todoStore`'s methods: - * - * - [can.fixture.types.Store.findAll findAll], - * - [can.fixture.types.Store.findOne findOne], - * - [can.fixture.types.Store.create create], - * - [can.fixture.types.Store.update update], and - * - [can.fixture.types.Store.destroy destroy] - * - * Can be used to simulate a REST service like: - * - * can.fixture({ - * 'GET /todos': todoStore.findAll, - * 'GET /todos/{id}': todoStore.findOne, - * 'POST /todos': todoStore.create, - * 'PUT /todos/{id}': todoStore.update, - * 'DELETE /todos/{id}': todoStore.destroy - * }); - * - * These fixtures, combined with a [can.Model] that connects to these services like: - * - * var Todo = can.Model.extend({ - * findAll : 'GET /todos', - * findOne : 'GET /todos/{id}', - * create : 'POST /todos', - * update : 'PUT /todos/{id}', - * destroy : 'DELETE /todos/{id}' - * }, {}); - * - * ... allows you to simulate requests for all of owner 5's todos like: - * - * Todo.findAll({ownerId: 5}, function(todos){ - * - * }) - * - * - */ + // ## can.fixture.store + // Make a store of objects to use when making requests against fixtures. store: function (count, make, filter) { /*jshint eqeqeq:false */ @@ -492,53 +455,6 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { // make all items can.extend(methods, { - /** - * @description Simulate a findAll to a fixture. - * @function can.fixture.types.Store.findAll - * @parent can.fixture.types.Store - * @signature `store.findAll(request)` - * - * `store.findAll(request)` simulates a request to - * get a list items from the server. It supports the - * following params: - * - * - order - `order=name ASC` - * - group - `group=name` - * - limit - `limit=20` - * - offset - `offset=60` - * - id filtering - `ownerId=5` - * - * - * @param {{}} request The ajax request object. The available parameters are: - * @option {String} order The order of the results. - * `order: 'name ASC'` - * @option {String} group How to group the results. - * `group: 'name'` - * @option {String} limit A limit on the number to retrieve. - * `limit: 20` - * @option {String} offset The offset of the results. - * `offset: 60` - * @option {String} id Filtering by ID. - * `id: 5` - * - * @return {Object} a response object like: - * - * { - * count: 1000, - * limit: 20, - * offset: 60, - * data: [item1, item2, ...] - * } - * - * where: - * - * - count - the number of items that match any filtering - * before limit and offset is taken into account - * - offset - the offset passed - * - limit - the limit passed - * - data - an array of JS objects with each item's properties - * - */ findAll: function (request) { request = request || {}; //copy array of items @@ -620,7 +536,8 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } } - //return data spliced with limit and offset + // Return the data spliced with limit and offset, along with related values + // (e.g. count, limit, offset) return { "count": retArr.length, "limit": request.data.limit, @@ -628,52 +545,19 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { "data": retArr.slice(offset, offset + limit) }; }, - /** - * @description Simulate a findOne request on a fixture. - * @function can.fixture.types.Store.findOne - * @parent can.fixture.types.Store - * @signature `store.findOne(request, callback)` - * @param {Object} request Parameters for the request. - * @param {Function} callback A function to call with the retrieved item. - * - * @body - * `store.findOne(request, response(item))` simulates a request to - * get a single item from the server by id. - * - * todosStore.findOne({ - * url: "/todos/5" - * }, function(todo){ - * - * }); - * - */ + // ## fixtureStore.findOne + // Simulates a can.Model.findOne to a fixture findOne: function (request, response) { var item = findOne(getId(request)); response(item ? item : undefined); }, - /** - * @description Simulate an update on a fixture. - * @function can.fixture.types.Store.update - * @parent can.fixture.types.Store - * @signature `store.update(request, callback)` - * @param {Object} request Parameters for the request. - * @param {Function} callback A function to call with the updated item and headers. - * - * @body - * `store.update(request, response(props,headers))` simulates - * a request to update an items properties on a server. - * - * todosStore.update({ - * url: "/todos/5" - * }, function(props, headers){ - * props.id //-> 5 - * headers.location // "todos/5" - * }); - */ + // ## fixtureStore.update + // Simulates a can.Model.update to a fixture update: function (request, response) { var id = getId(request); // TODO: make it work with non-linear ids .. + // Retrieve item that matched ID, and merge request data into it. can.extend(findOne(id), request.data); response({ id: getId(request) @@ -681,24 +565,9 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { location: request.url || "/" + getId(request) }); }, - /** - * @description Simulate destroying a Model on a fixture. - * @function can.fixture.types.Store.destroy - * @parent can.fixture.types.Store - * @signature `store.destroy(request, callback)` - * @param {Object} request Parameters for the request. - * @param {Function} callback A function to call after destruction. - * - * @body - * `store.destroy(request, response())` simulates - * a request to destroy an item from the server. - * - * @codestart - * todosStore.destroy({ - * url: "/todos/5" - * }, function(){}); - * @codeend - */ + + // ## fixtureStore.destroy + // Simulates a can.Model.destroy to a fixture destroy: function (request) { var id = getId(request); for (var i = 0; i < items.length; i++) { @@ -712,33 +581,21 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { can.extend(findOne(id) || {}, request.data); return {}; }, - /** - * @description Simulate creating a Model with a fixture. - * @function can.fixture.types.Store.create - * @parent can.fixture.types.Store - * @signature `store.create(request, callback)` - * @param {Object} request Parameters for the request. - * @param {Function} callback A function to call with the created item. - * - * @body - * `store.destroy(request, callback)` simulates - * a request to destroy an item from the server. - * - * @codestart - * todosStore.create({ - * url: "/todos" - * }, function(){}); - * @codeend - */ + + // ## fixtureStore.create + // Simulates a can.Model.create to a fixture create: function (settings, response) { var item = make(items.length, items); can.extend(item, settings.data); + // If an ID wasn't passed into the request, we give the item + // a unique ID. if (!item.id) { item.id = currentId++; } + // Push the new item into the store. items.push(item); response({ id: item.id @@ -752,97 +609,12 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { return can.extend({ getId: getId, - /** - * @description Get an item from the store by ID. - * @function can.fixture.types.Store.find - * @parent can.fixture.types.Store - * @signature `store.find(settings)` - * @param {Object} settings An object containing an `id` key - * corresponding to the item to find. - * - * @body - * `store.find(settings)` - * `store.destroy(request, callback)` simulates a request to - * get a single item from the server. - * - * @codestart - * todosStore.find({ - * url: "/todos/5" - * }, function(){}); - * @codeend - */ find: function (settings) { return findOne(getId(settings)); }, - /** - * @description Reset the fixture store. - * @function can.fixture.types.Store.reset - * @parent can.fixture.types.Store - * @signature `store.reset()` - * - * @body - * `store.reset()` resets the store to contain its - * original data. This is useful for making tests that - * operate independently. - * - * ## Basic Example - * - * After creating a `taskStore` and hooking it up to a - * `task` model in the "Basic Example" in [can.fixture.store store's docs], - * a test might create several tasks like: - * - * new Task({name: "Take out trash", ownerId: 5}).save(); - * - * But, another test might need to operate on the original set of - * tasks created by `can.fixture.store`. Reset the task store with: - * - * taskStore.reset() - * - */ reset: reset }, methods); }, - /** - * @description Create a random number or selection. - * @function can.fixture.rand rand - * @parent can.fixture - * @signature `can.fixture.rand([min,] max)` - * @param {Number} [min=0] The lower bound on integers to select. - * @param {Number} max The upper bound on integers to select. - * @return {Number} A random integer in the range [__min__, __max__). - * - * @signature `can.fixture.rand(choices, min[ ,max])` - * @param {Array} choices An array of things to choose from. - * @param {Number} min The minimum number of times to choose from __choices__. - * @param {Number} [max=min] The maximum number of times to choose from __choices__. - * @return {Array} An array of between __min__ and __max__ random choices from __choices__. - * - * @body - * `can.fixture.rand` creates random integers or random arrays of - * other arrays. - * - * ## Examples - * - * var rand = can.fixture.rand; - * - * // get a random integer between 0 and 10 (inclusive) - * rand(11); - * - * // get a random number between -5 and 5 (inclusive) - * rand(-5, 6); - * - * // pick a random item from an array - * rand(["j","m","v","c"],1)[0] - * - * // pick a random number of items from an array - * rand(["j","m","v","c"]) - * - * // pick 2 items from an array - * rand(["j","m","v","c"],2) - * - * // pick between 2 and 3 items at random - * rand(["j","m","v","c"],2,3) - */ rand: function randomize(arr, min, max) { if (typeof arr === 'number') { if (typeof min === 'number') { @@ -871,37 +643,6 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { } return res; }, - /** - * @hide - * - * Use can.fixture.xhr to create an object that looks like an xhr object. - * - * ## Example - * - * The following example shows how the -restCreate fixture uses xhr to return - * a simulated xhr object: - * @codestart - * "-restCreate" : function( settings, cbType ) { - * switch(cbType){ - * case "success": - * return [ - * {id: parseInt(Math.random()*1000)}, - * "success", - * can.fixture.xhr()]; - * case "complete": - * return [ - * can.fixture.xhr({ - * getResponseHeader: function() { - * return settings.url+"/"+parseInt(Math.random()*1000); - * } - * }), - * "success"]; - * } - * } - * @codeend - * @param {Object} [xhr] properties that you want to overwrite - * @return {Object} an object that looks like a successful XHR object. - */ xhr: function (xhr) { return can.extend({}, { abort: can.noop, @@ -922,43 +663,16 @@ steal('can/util', 'can/util/string', 'can/util/object', function (can) { statusText: "OK" }, xhr); }, - /** - * @property {Boolean} can.fixture.on on - * @parent can.fixture - * - * `can.fixture.on` lets you programatically turn off fixtures. This is mostly used for testing. - * - * can.fixture.on = false - * Task.findAll({}, function(){ - * can.fixture.on = true; - * }) - */ on: true }); - /** - * @property {Number} can.fixture.delay delay - * @parent can.fixture - * - * `can.fixture.delay` indicates the delay in milliseconds between an ajax request is made and - * the success and complete handlers are called. This only sets - * functional synchronous fixtures that return a result. By default, the delay is 200ms. - * - * @codestart - * steal('can/util/fixtures').then(function(){ - * can.fixture.delay = 1000; - * }) - * @codeend - */ + + // ## can.fixture.delay + // The delay, in milliseconds, between an AJAX request being made and when + // the success callback gets called. can.fixture.delay = 200; - /** - * @property {String} can.fixture.rootUrl rootUrl - * @parent can.fixture - * - * `can.fixture.rootUrl` contains the root URL for fixtures to use. - * If you are using StealJS it will use the Steal root - * URL by default. - */ + // ## can.fixture.rootUrl + // The root URL which fixtures will use. can.fixture.rootUrl = getUrl(''); can.fixture["-handleFunction"] = function (settings) { diff --git a/util/fixture/fixture_test.js b/util/fixture/fixture_test.js index b605bab9966..3ba5ae81c8e 100644 --- a/util/fixture/fixture_test.js +++ b/util/fixture/fixture_test.js @@ -453,6 +453,21 @@ steal('can/util/fixture', 'can/model', 'can/test', function () { equal(responseData.id, 2, 'the third id is 2'); }); }); + + test('fixture updates request.data with id', function() { + expect(1); + stop(); + + + can.fixture('foo/{id}', function(request) { + equal(request.data.id, 5); + start(); + }); + + can.ajax({ + url: 'foo/5' + }); + }); test("create a store with array and comparison object",function(){ @@ -470,18 +485,13 @@ steal('can/util/fixture', 'can/model', 'can/test', function () { can.fixture('GET /presetStore', store.findAll); stop(); - can.ajax({ - url: "/presetStore", - dataType: 'json', - data: {year: 2013, modelId:1} - }).done(function(response){ + can.ajax({ url: "/presetStore", method: "get", data: {year: 2013, modelId:1} }).then(function(response){ equal(response.data[0].id, 1, "got the first item"); equal(response.data.length, 1, "only got one item"); start(); }); - }); diff --git a/util/fixture/xhr.md b/util/fixture/xhr.md deleted file mode 100644 index 75a124ee67d..00000000000 --- a/util/fixture/xhr.md +++ /dev/null @@ -1,7 +0,0 @@ -@typedef {{}} can.AjaxSettings - -@description The options available to be passed to [can.ajax]. - -@option {String} url -@option {*} data - diff --git a/util/inserted/inserted.js b/util/inserted/inserted.js index 5ad6fcf7cfb..c8a0fce0167 100644 --- a/util/inserted/inserted.js +++ b/util/inserted/inserted.js @@ -1,13 +1,17 @@ +// # can/util/inserted +// Used to alert interested parties of when an element is inserted into the DOM. +// Given a list of elements, check if the first is in the DOM, and if so triggers the `inserted` event on all elements and their descendants. + steal('can/util/can.js', function (can) { - // Given a list of elements, check if they are in the dom, if they - // are in the dom, trigger inserted on them. can.inserted = function (elems) { - // prevent mutations from changing the looping + // Turn the `elems` property into an array to prevent mutations from changing the looping. elems = can.makeArray(elems); var inDocument = false, - // Not all browsers implement document.contains (Android) + // Gets the `doc` to use as a reference for finding out whether the element is in the document. doc = can.$(document.contains ? document : document.body), children; + // Go through `elems` and trigger the `inserted` event. + // If the first element is not in the document (a Document Fragment) it will exit the function. If it is in the document it sets the `inDocument` flag to true. This means that we only check for the first element and either exit the function or start triggering "inserted" for child elements. for (var i = 0, elem; (elem = elems[i]) !== undefined; i++) { if (!inDocument) { @@ -23,18 +27,20 @@ steal('can/util/can.js', function (can) { } } + // If we've found an element in the document then we can now trigger **"inserted"** for `elem` and all of its children. We are using `getElementsByTagName("*")` so that we grab all of the descendant nodes. if (inDocument && elem.getElementsByTagName) { children = can.makeArray(elem.getElementsByTagName("*")); can.trigger(elem, "inserted", [], false); for (var j = 0, child; (child = children[j]) !== undefined; j++) { - // Trigger the destroyed event can.trigger(child, "inserted", [], false); } } } }; + // ## can.appendChild + // Used to append a node to an element and trigger the "inserted" event on all of the newly inserted children. Since `can.inserted` takes an array we convert the child to an array, or in the case of a DocumentFragment we first convert the childNodes to an array and call inserted on those. can.appendChild = function (el, child) { var children; if (child.nodeType === 11) { @@ -45,6 +51,9 @@ steal('can/util/can.js', function (can) { el.appendChild(child); can.inserted(children); }; + + // ## can.insertBefore + // Like can.appendChild, used to insert a node to an element before a reference node and then trigger the "inserted" event. can.insertBefore = function (el, child, ref) { var children; if (child.nodeType === 11) { @@ -55,5 +64,4 @@ steal('can/util/can.js', function (can) { el.insertBefore(child, ref); can.inserted(children); }; - }); diff --git a/view/bindings/bindings.js b/view/bindings/bindings.js index 9ef4ccc09c2..16c6643c342 100644 --- a/view/bindings/bindings.js +++ b/view/bindings/bindings.js @@ -1,62 +1,25 @@ +// # can/view/bindings/bindings.js +// +// This file defines the `can-value` attribute for two-way bindings and the `can-EVENT` attribute +// for in template event bindings. These are usable in any mustache template, but mainly and documented +// for use within can.Component. steal("can/util", "can/view/mustache", "can/control", function (can) { - /** - * @function can.view.bindings.can-value can-value - * @parent can.view.bindings - * - * Sets up two way bindings in a template. - * - * @signature `can-value='KEY'` - * - * Binds the element's value or checked property to the value specified by - * key. Example: - * - * - * - * @param {can.Mustache.key} key A named value in the current scope. - * - * @body - * - * ## Use - * - * Add a `can-value="KEY"` attribute to an input or select element and - * the element's value will be cross-bound to an observable value specified by `KEY`. - * - * Depending on the element and the element's type, `can-value` takes on - * different behaviors. If an input element has a type - * not listed here, the behavior is the same as the `text` type. - * - * ## input type=text - * - * Cross binds the input's string text value with the observable value. - * - * @demo can/view/bindings/hyperloop.html - * - * ## input type=checkbox - * - * Cross binds the checked property to a true or false value. An alternative - * true and false value can be specified by setting `can-true-value` and - * `can-false-value` attributes. - * - * @demo can/view/bindings/input-checkbox.html - * - * ## input type='radio' - * - * If the radio element is checked, sets the observable specified by `can-value` to match the value of - * `value` attribute. - * - * @demo can/view/bindings/input-radio.html - * - * ## select - * - * Cross binds the selected option value with an observable value. - * - * @demo can/view/bindings/select.html - * - */ + // ## can-value + // Implement the `can-value` special attribute + // + // ### Usage + // + // + // + // When a view engine finds this attribute, it will call this callback. The value of the attribute + // should be a string representing some value in the current scope to cross-bind to. can.view.attr("can-value", function (el, data) { var attr = el.getAttribute("can-value"), + // Turn the attribute passed in into a compute. If the user passed in can-value="name" and the current + // scope of the template is some object called data, the compute representing this can-value will be the + // data.attr('name') property. value = data.scope.computeData(attr, { args: [] }) @@ -64,21 +27,31 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { trueValue, falseValue; + // Depending on the type of element, this attribute has different behavior. can.Controls are defined (further below + // in this file) for each type of input. This block of code collects arguments and instantiates each can.Control. There + // is one for checkboxes/radios, another for multiselect inputs, and another for everything else. if (el.nodeName.toLowerCase() === "input") { if (el.type === "checkbox") { + // If the element is a checkbox and has an attribute called "can-true-value", + // set up a compute that toggles the value of the checkbox to "true" based on another attribute. + // + // if (can.attr.has(el, "can-true-value")) { - trueValue = data.scope.compute(el.getAttribute("can-true-value")); + trueValue = el.getAttribute("can-true-value"); } else { - trueValue = can.compute(true); + trueValue = true; } if (can.attr.has(el, "can-false-value")) { - falseValue = data.scope.compute(el.getAttribute("can-false-value")); + falseValue = el.getAttribute("can-false-value"); } else { - falseValue = can.compute(false); + falseValue = false; } } if (el.type === "checkbox" || el.type === "radio") { + // For checkboxes and radio buttons, create a Checked can.Control around the input. Pass in + // the compute representing the can-value and can-true-value and can-false-value properties (if + // they were used). new Checked(el, { value: value, trueValue: trueValue, @@ -88,16 +61,35 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { } } if (el.nodeName.toLowerCase() === "select" && el.multiple) { + // For multiselect enabled select inputs, we instantiate a special control around that select element + // called Multiselect new Multiselect(el, { value: value }); return; } + // The default case. Instantiate the Value control around the element. Pass it the compute representing + // the observable attribute property that was set. new Value(el, { value: value }); }); + // ## Special Event Types (can-SPECIAL) + + // A special object, similar to [$.event.special](http://benalman.com/news/2010/03/jquery-special-events/), + // for adding hooks for special can-SPECIAL types (not native DOM events). Right now, only can-enter is + // supported, but this object might be exported so that it can be added to easily. + // + // To implement a can-SPECIAL event type, add a property to the special object, whose value is a function + // that returns the following: + // + // // the real event name to bind to + // event: "event-name", + // handler: function (ev) { + // // some logic that figures out if the original handler should be called or not, and if so... + // return original.call(this, ev); + // } var special = { enter: function (data, el, original) { return { @@ -111,40 +103,24 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { } }; - /** - * @function can.view.bindings.can-EVENT can-EVENT - * @parent can.view.bindings - * - * @signature `can-EVENT='KEY'` - * - * Specify a callback function to be called on a particular event. - * - * @param {String} EVENT A event name like `click` or `keyup`. If you are - * using jQuery, you can listen to jQuery special events too. - * - * @param {can.Mustache.key} key A named value in the current scope. The value - * should be a function. - * - * @body - * - * ## Use - * - * By adding `can-EVENT='KEY'` to an element, the function pointed to - * by `KEY` is bound to the element's `EVENT` event. The function - * is called back with: - * - * - `context` - the context of the element - * - `element` - the element that was bound - * - `event` - the event that was triggered - * - * @demo can/view/bindings/can-event.html - * - */ + // ## can-EVENT + // The following section contains code for implementing the can-EVENT attribute. + // This binds on a wildcard attribute name. Whenever a view is being processed + // and can-xxx (anything starting with can-), this callback will be run. Inside, its setting up an event handler + // that calls a method identified by the value of this attribute. can.view.attr(/can-[\w\.]+/, function (el, data) { + // the attribute name is the function to call var attributeName = data.attributeName, + // The event type to bind on is deteremined by whatever is after can- + // + // For example, can-submit binds on the submit event. event = attributeName.substr("can-".length), + // This is the method that the event will initially trigger. It will look up the method by the string name + // passed in the attribute and call it. handler = function (ev) { + // The attribute value, representing the name of the method to call (i.e. can-submit="foo" foo is the + // name of the method) var attr = el.getAttribute(attributeName), scopeData = data.scope.read(attr, { returnObserveMethods: true, @@ -153,56 +129,85 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { return scopeData.value.call(scopeData.parent, data.scope._context, can.$(this), ev); }; + // This code adds support for special event types, like can-enter="foo". special.enter (or any special[event]) is + // a function that returns an object containing an event and a handler. These are to be used for binding. For example, + // when a user adds a can-enter attribute, we'll bind on the keyup event, and the handler performs special logic to + // determine on keyup if the enter key was pressed. if (special[event]) { var specialData = special[event](data, el, handler); handler = specialData.handler; event = specialData.event; } + // Bind the handler defined above to the element we're currently processing and the event name provided in this + // attribute name (can-click="foo") can.bind.call(el, event, handler); }); + + // ## Two way binding can.Controls + // Each type of input that is supported by view/bindings is wrapped with a special can.Control. The control serves + // two functions: + // 1. Bind on the property changing (the compute we're two-way binding to) and change the input value. + // 2. Bind on the input changing and change the property (compute) we're two-way binding to. + // There is one control per input type. There could easily be more for more advanced input types, like the HTML5 type="date" input type. + + + // ### Value + // A can.Control that manages the two-way bindings on most inputs. When can-value is found as an attribute + // on an input, the callback above instantiates this Value control on the input element. var Value = can.Control.extend({ init: function () { + // Handle selects by calling `set` after this thread so the rest of the element can finish rendering. if (this.element[0].nodeName.toUpperCase() === "SELECT") { - // need to wait until end of turn ... setTimeout(can.proxy(this.set, this), 1); } else { this.set(); } }, + // If the live bound data changes, call set to reflect the change in the dom. "{value} change": "set", set: function () { - //this may happen in some edgecases, esp. with selects that are not in DOM after the timeout has fired + // This may happen in some edgecases, esp. with selects that are not in DOM after the timeout has fired if (!this.element) { return; } var val = this.options.value(); + // Set the element's value to match the attribute that was passed in this.element[0].value = (typeof val === 'undefined' ? '' : val); }, + // If the input value changes, this will set the live bound data to reflect the change. "change": function () { - //this may happen in some edgecases, esp. with selects that are not in DOM after the timeout has fired + // This may happen in some edgecases, esp. with selects that are not in DOM after the timeout has fired if (!this.element) { return; } + // Set the value of the attribute passed in to reflect what the user typed this.options.value(this.element[0].value); } }), + // ### Checked + // A can.Control that manages the two-way bindings on a checkbox element. When can-value is found as an attribute + // on a checkbox, the callback above instantiates this Checked control on the checkbox element. Checked = can.Control.extend({ init: function () { - this.isCheckebox = (this.element[0].type.toLowerCase() === "checkbox"); + // If its not a checkbox, its a radio input + this.isCheckbox = (this.element[0].type.toLowerCase() === "checkbox"); this.check(); }, + // `value` is the compute representing the can-value for this element. For example can-value="foo" and current + // scope is someObj, value is the compute representing someObj.attr('foo') "{value} change": "check", - "{trueValue} change": "check", - "{falseValue} change": "check", check: function () { - if (this.isCheckebox) { + if (this.isCheckbox) { var value = this.options.value(), - trueValue = this.options.trueValue() || true; - + trueValue = this.options.trueValue || true; + // If `can-true-value` attribute was set, check if the value is equal to that string value, and set + // the checked property based on their equality. this.element[0].checked = (value === trueValue); - } else { + } + // Its a radio input type + else { var setOrRemove = this.options.value() === this.element[0].value ? "set" : "remove"; @@ -211,11 +216,16 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { } }, + // This event is triggered by the DOM. If a change event occurs, we must set the value of the compute (options.value). "change": function () { - if (this.isCheckebox) { - this.options.value(this.element[0].checked ? this.options.trueValue() : this.options.falseValue()); - } else { + if (this.isCheckbox) { + // If the checkbox is checked and can-true-value was used, set value to the string value of can-true-value. If + // can-false-value was used and checked is false, set value to the string value of can-false-value. + this.options.value(this.element[0].checked ? this.options.trueValue : this.options.falseValue); + } + // Radio input type + else { if (this.element[0].checked) { this.options.value(this.element[0].value); } @@ -223,31 +233,37 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { } }), + // ### Multiselect + // A can.Control that handles select input with the "multiple" attribute (meaning more than one can be selected at once). Multiselect = Value.extend({ init: function () { this.delimiter = ";"; this.set(); }, - + // Since this control extends Value (above), the set method will be called when the value compute changes (and on init). set: function () { var newVal = this.options.value(); + + // When given a string, try to extract all the options from it (i.e. "a;b;c;d") if (typeof newVal === 'string') { - //when given a string, try to extract all the options from it newVal = newVal.split(this.delimiter); this.isString = true; - } else if (newVal) { - //when given something else, try to make it an array and deal with it + } + // When given something else, try to make it an array and deal with it + else if (newVal) { newVal = can.makeArray(newVal); } - //jQuery.val is required here, which will break compatibility with other libs + // Make an object containing all the options passed in for convenient lookup var isSelected = {}; can.each(newVal, function (val) { isSelected[val] = true; }); + // Go through each <option/> element, if it has a value property (its a valid option), then + // set its selected property if it was in the list of vals that were just set. can.each(this.element[0].childNodes, function (option) { if (option.value) { option.selected = !! isSelected[option.value]; @@ -256,7 +272,8 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { }); }, - + // A helper function used by the 'change' handler below. Its purpose is to return an array of selected + // values, like ["foo", "bar"] get: function () { var values = [], children = this.element[0].childNodes; @@ -269,17 +286,23 @@ steal("can/util", "can/view/mustache", "can/control", function (can) { return values; }, - + // Called when the user changes this input in any way. 'change': function () { + // Get an array of the currently selected values var value = this.get(), currentValue = this.options.value(); + // If the compute is a string, set its value to the joined version of the values array (i.e. "foo;bar") if (this.isString || typeof currentValue === "string") { this.isString = true; this.options.value(value.join(this.delimiter)); - } else if (currentValue instanceof can.List) { + } + // If the compute is a can.List, replace its current contents with the new array of values + else if (currentValue instanceof can.List) { currentValue.attr(value, true); - } else { + } + // Otherwise set the value to the array of values selected in the input. + else { this.options.value(value); } diff --git a/view/bindings/bindings_test.js b/view/bindings/bindings_test.js index 0c813184b23..c9fbf7b8dae 100644 --- a/view/bindings/bindings_test.js +++ b/view/bindings/bindings_test.js @@ -1,4 +1,4 @@ -steal("can/view/bindings", "can/map", "can/test", function () { +steal("can/view/bindings", "can/map", "can/test", function (special) { module('can/view/bindings', { setup: function () { document.getElementById("qunit-test-area") @@ -204,6 +204,27 @@ steal("can/view/bindings", "can/map", "can/test", function () { equal(data.attr('completed'), false, 'checkbox value bound (via uncheck)'); }); + test("checkboxes with can-true-value bind properly", function () { + var data = new can.Map({ + sex: "male" + }), + frag = can.view.mustache('')(data); + can.append(can.$("#qunit-test-area"), frag); + + var input = can.$("#qunit-test-area")[0].getElementsByTagName('input')[0]; + equal(input.checked, true, 'checkbox value bound (via attr check)'); + data.attr('sex', 'female'); + equal(input.checked, false, 'checkbox value unbound (via attr uncheck)'); + input.checked = true; + can.trigger(input, 'change'); + equal(input.checked, true, 'checkbox value bound (via check)'); + equal(data.attr('sex'), 'male', 'checkbox value bound (via check)'); + input.checked = false; + can.trigger(input, 'change'); + equal(input.checked, false, 'checkbox value bound (via uncheck)'); + equal(data.attr('sex'), 'female', 'checkbox value bound (via uncheck)'); + }); + test("can-value select single", function () { var template = can.view.mustache( diff --git a/view/bindings/bindings.md b/view/bindings/doc/bindings.md similarity index 90% rename from view/bindings/bindings.md rename to view/bindings/doc/bindings.md index 6b116abe275..a17521e2d33 100644 --- a/view/bindings/bindings.md +++ b/view/bindings/doc/bindings.md @@ -1,5 +1,6 @@ @page can.view.bindings @parent canjs +@link ../docco/view/bindings/bindings.html docco Provides template event bindings and two-way bindings. @@ -25,4 +26,4 @@ call a function in a [can.Mustache Mustache] [can.view.Scope scope] when an even Two-way bindings are documented by [can.view.bindings.can-value can-value]. This lets you listen to when element changes its value and automatically update an observable property. -@demo can/view/bindings/hyperloop.html \ No newline at end of file +@demo can/view/bindings/doc/hyperloop.html \ No newline at end of file diff --git a/view/bindings/can-event.html b/view/bindings/doc/can-event.html similarity index 92% rename from view/bindings/can-event.html rename to view/bindings/doc/can-event.html index 5b69bcf0a01..a3f0c957a4e 100644 --- a/view/bindings/can-event.html +++ b/view/bindings/doc/can-event.html @@ -1,5 +1,5 @@
- + + + + diff --git a/view/bindings/input-checkbox.html b/view/bindings/doc/input-checkbox.html similarity index 93% rename from view/bindings/input-checkbox.html rename to view/bindings/doc/input-checkbox.html index 40fb2e41e0e..ae19b2dd28d 100644 --- a/view/bindings/input-checkbox.html +++ b/view/bindings/doc/input-checkbox.html @@ -1,5 +1,5 @@
- + - + + - + - + - + - + + - +