Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <title>
        FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 2
    </title>
    <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
</head>
<body>
    <h1>
        FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 2
    </h1>
    <h2>
        With ngModel - Bridging The Gap
    </h2>
    <my-app>
        Loading...
    </my-app>
    <!-- Load demo scripts. -->
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.2/Rx.umd.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.2/angular2-polyfills.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/2.0.0-beta.2/angular2-all.umd.js"></script>
    <!-- AlmondJS - minimal implementation of RequireJS. -->
    <script type="text/javascript">
/**
 * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved.
 * Available via the MIT or new BSD license.
 * see: http://github.com/jrburke/almond for details
 */
//Going sloppy to avoid 'use strict' string cost, but strict practices should
//be followed.
/*jslint sloppy: true */
/*global setTimeout: false */
var requirejs, require, define;
(function (undef) {
    var main, req, makeMap, handlers,
        defined = {},
        waiting = {},
        config = {},
        defining = {},
        hasOwn = Object.prototype.hasOwnProperty,
        aps = [].slice,
        jsSuffixRegExp = /\.js$/;
    function hasProp(obj, prop) {
        return hasOwn.call(obj, prop);
    }
    /**
     * Given a relative module name, like ./something, normalize it to
     * a real name that can be mapped to a path.
     * @param {String} name the relative name
     * @param {String} baseName a real name that the name arg is relative
     * to.
     * @returns {String} normalized name
     */
    function normalize(name, baseName) {
        var nameParts, nameSegment, mapValue, foundMap, lastIndex,
            foundI, foundStarMap, starI, i, j, part,
            baseParts = baseName && baseName.split("/"),
            map = config.map,
            starMap = (map && map['*']) || {};
        //Adjust any relative paths.
        if (name && name.charAt(0) === ".") {
            //If have a base name, try to normalize against it,
            //otherwise, assume it is a top-level require that will
            //be relative to baseUrl in the end.
            if (baseName) {
                name = name.split('/');
                lastIndex = name.length - 1;
                // Node .js allowance:
                if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
                    name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
                }
                //Lop off the last part of baseParts, so that . matches the
                //"directory" and not name of the baseName's module. For instance,
                //baseName of "one/two/three", maps to "one/two/three.js", but we
                //want the directory, "one/two" for this normalization.
                name = baseParts.slice(0, baseParts.length - 1).concat(name);
                //start trimDots
                for (i = 0; i < name.length; i += 1) {
                    part = name[i];
                    if (part === ".") {
                        name.splice(i, 1);
                        i -= 1;
                    } else if (part === "..") {
                        if (i === 1 && (name[2] === '..' || name[0] === '..')) {
                            //End of the line. Keep at least one non-dot
                            //path segment at the front so it can be mapped
                            //correctly to disk. Otherwise, there is likely
                            //no path mapping for a path starting with '..'.
                            //This can still fail, but catches the most reasonable
                            //uses of ..
                            break;
                        } else if (i > 0) {
                            name.splice(i - 1, 2);
                            i -= 2;
                        }
                    }
                }
                //end trimDots
                name = name.join("/");
            } else if (name.indexOf('./') === 0) {
                // No baseName, so this is ID is resolved relative
                // to baseUrl, pull off the leading dot.
                name = name.substring(2);
            }
        }
        //Apply map config if available.
        if ((baseParts || starMap) && map) {
            nameParts = name.split('/');
            for (i = nameParts.length; i > 0; i -= 1) {
                nameSegment = nameParts.slice(0, i).join("/");
                if (baseParts) {
                    //Find the longest baseName segment match in the config.
                    //So, do joins on the biggest to smallest lengths of baseParts.
                    for (j = baseParts.length; j > 0; j -= 1) {
                        mapValue = map[baseParts.slice(0, j).join('/')];
                        //baseName segment has  config, find if it has one for
                        //this name.
                        if (mapValue) {
                            mapValue = mapValue[nameSegment];
                            if (mapValue) {
                                //Match, update name to the new value.
                                foundMap = mapValue;
                                foundI = i;
                                break;
                            }
                        }
                    }
                }
                if (foundMap) {
                    break;
                }
                //Check for a star map match, but just hold on to it,
                //if there is a shorter segment match later in a matching
                //config, then favor over this star map.
                if (!foundStarMap && starMap && starMap[nameSegment]) {
                    foundStarMap = starMap[nameSegment];
                    starI = i;
                }
            }
            if (!foundMap && foundStarMap) {
                foundMap = foundStarMap;
                foundI = starI;
            }
            if (foundMap) {
                nameParts.splice(0, foundI, foundMap);
                name = nameParts.join('/');
            }
        }
        return name;
    }
    function makeRequire(relName, forceSync) {
        return function () {
            //A version of a require function that passes a moduleName
            //value for items that may need to
            //look up paths relative to the moduleName
            var args = aps.call(arguments, 0);
            //If first arg is not require('string'), and there is only
            //one arg, it is the array form without a callback. Insert
            //a null so that the following concat is correct.
            if (typeof args[0] !== 'string' && args.length === 1) {
                args.push(null);
            }
            return req.apply(undef, args.concat([relName, forceSync]));
        };
    }
    function makeNormalize(relName) {
        return function (name) {
            return normalize(name, relName);
        };
    }
    function makeLoad(depName) {
        return function (value) {
            defined[depName] = value;
        };
    }
    function callDep(name) {
        if (hasProp(waiting, name)) {
            var args = waiting[name];
            delete waiting[name];
            defining[name] = true;
            main.apply(undef, args);
        }
        if (!hasProp(defined, name) && !hasProp(defining, name)) {
            throw new Error('No ' + name);
        }
        return defined[name];
    }
    //Turns a plugin!resource to [plugin, resource]
    //with the plugin being undefined if the name
    //did not have a plugin prefix.
    function splitPrefix(name) {
        var prefix,
            index = name ? name.indexOf('!') : -1;
        if (index > -1) {
            prefix = name.substring(0, index);
            name = name.substring(index + 1, name.length);
        }
        return [prefix, name];
    }
    /**
     * Makes a name map, normalizing the name, and using a plugin
     * for normalization if necessary. Grabs a ref to plugin
     * too, as an optimization.
     */
    makeMap = function (name, relName) {
        var plugin,
            parts = splitPrefix(name),
            prefix = parts[0];
        name = parts[1];
        if (prefix) {
            prefix = normalize(prefix, relName);
            plugin = callDep(prefix);
        }
        //Normalize according
        if (prefix) {
            if (plugin && plugin.normalize) {
                name = plugin.normalize(name, makeNormalize(relName));
            } else {
                name = normalize(name, relName);
            }
        } else {
            name = normalize(name, relName);
            parts = splitPrefix(name);
            prefix = parts[0];
            name = parts[1];
            if (prefix) {
                plugin = callDep(prefix);
            }
        }
        //Using ridiculous property names for space reasons
        return {
            f: prefix ? prefix + '!' + name : name, //fullName
            n: name,
            pr: prefix,
            p: plugin
        };
    };
    function makeConfig(name) {
        return function () {
            return (config && config.config && config.config[name]) || {};
        };
    }
    handlers = {
        require: function (name) {
            return makeRequire(name);
        },
        exports: function (name) {
            var e = defined[name];
            if (typeof e !== 'undefined') {
                return e;
            } else {
                return (defined[name] = {});
            }
        },
        module: function (name) {
            return {
                id: name,
                uri: '',
                exports: defined[name],
                config: makeConfig(name)
            };
        }
    };
    main = function (name, deps, callback, relName) {
        var cjsModule, depName, ret, map, i,
            args = [],
            callbackType = typeof callback,
            usingExports;
        //Use name if no relName
        relName = relName || name;
        //Call the callback to define the module, if necessary.
        if (callbackType === 'undefined' || callbackType === 'function') {
            //Pull out the defined dependencies and pass the ordered
            //values to the callback.
            //Default to [require, exports, module] if no deps
            deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps;
            for (i = 0; i < deps.length; i += 1) {
                map = makeMap(deps[i], relName);
                depName = map.f;
                //Fast path CommonJS standard dependencies.
                if (depName === "require") {
                    args[i] = handlers.require(name);
                } else if (depName === "exports") {
                    //CommonJS module spec 1.1
                    args[i] = handlers.exports(name);
                    usingExports = true;
                } else if (depName === "module") {
                    //CommonJS module spec 1.1
                    cjsModule = args[i] = handlers.module(name);
                } else if (hasProp(defined, depName) ||
                           hasProp(waiting, depName) ||
                           hasProp(defining, depName)) {
                    args[i] = callDep(depName);
                } else if (map.p) {
                    map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {});
                    args[i] = defined[depName];
                } else {
                    throw new Error(name + ' missing ' + depName);
                }
            }
            ret = callback ? callback.apply(defined[name], args) : undefined;
            if (name) {
                //If setting exports via "module" is in play,
                //favor that over return value and exports. After that,
                //favor a non-undefined return value over exports use.
                if (cjsModule && cjsModule.exports !== undef &&
                        cjsModule.exports !== defined[name]) {
                    defined[name] = cjsModule.exports;
                } else if (ret !== undef || !usingExports) {
                    //Use the return value from the function.
                    defined[name] = ret;
                }
            }
        } else if (name) {
            //May just be an object definition for the module. Only
            //worry about defining if have a module name.
            defined[name] = callback;
        }
    };
    requirejs = require = req = function (deps, callback, relName, forceSync, alt) {
        if (typeof deps === "string") {
            if (handlers[deps]) {
                //callback in this case is really relName
                return handlers[deps](callback);
            }
            //Just return the module wanted. In this scenario, the
            //deps arg is the module name, and second arg (if passed)
            //is just the relName.
            //Normalize module name, if it contains . or ..
            return callDep(makeMap(deps, callback).f);
        } else if (!deps.splice) {
            //deps is a config object, not an array.
            config = deps;
            if (config.deps) {
                req(config.deps, config.callback);
            }
            if (!callback) {
                return;
            }
            if (callback.splice) {
                //callback is an array, which means it is a dependency list.
                //Adjust args if there are dependencies
                deps = callback;
                callback = relName;
                relName = null;
            } else {
                deps = undef;
            }
        }
        //Support require(['a'])
        callback = callback || function () {};
        //If relName is a function, it is an errback handler,
        //so remove it.
        if (typeof relName === 'function') {
            relName = forceSync;
            forceSync = alt;
        }
        //Simulate async callback;
        if (forceSync) {
            main(undef, deps, callback, relName);
        } else {
            //Using a non-zero value because of concern for what old browsers
            //do, and latest browsers "upgrade" to 4 if lower value is used:
            //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout:
            //If want a value immediately, use require('id') instead -- something
            //that works in almond on the global level, but not guaranteed and
            //unlikely to work in other AMD implementations.
            setTimeout(function () {
                main(undef, deps, callback, relName);
            }, 4);
        }
        return req;
    };
    /**
     * Just drops the config on the floor, but returns req in case
     * the config return value is used.
     */
    req.config = function (cfg) {
        return req(cfg);
    };
    /**
     * Expose module registry for debugging and tooling
     */
    requirejs._defined = defined;
    define = function (name, deps, callback) {
        if (typeof name !== 'string') {
            throw new Error('See almond README: incorrect module build, no module name');
        }
        //This module may not have dependencies
        if (!deps.splice) {
            //deps is not an array, so probably means
            //an object literal or factory function for
            //the value. Adjust args.
            callback = deps;
            deps = [];
        }
        if (!hasProp(defined, name) && !hasProp(waiting, name)) {
            waiting[name] = [name, deps, callback];
        }
    };
    define.amd = {
        jQuery: true
    };
}());     
      
    </script>
    <script type="text/javascript">
        // Defer bootstrapping until all of the components have been declared.
        // --
        // NOTE: Not all components have to be required here since they will be
        // implicitly required by other components.
        requirejs(
            [ "AppComponent" ],
            function run( AppComponent ) {
                // DO NOT DO THIS! There are many answers on the net that say to
                // enable "production mode" in order to get rid of the following error:
                // --
                // Expression '...' has changed after it was checked.
                // --
                // DO NOT DO THIS! It doesn't actually work. Parts of it may look like
                // it is working, but part of the data are desynchronizing. This becomes
                // obvious when you have multiple toggles on the page, one of which is
                // not using ngModel.
                // --
                // ng.core.enableProdMode();
                ng.platform.browser.bootstrap( AppComponent );
            }
        );
        // --------------------------------------------------------------------------- //
        // --------------------------------------------------------------------------- //
        // I provide the root application component.
        define(
            "AppComponent",
            function registerAppComponent() {
                // NOTE: We are including a DIFFERENT DIRECTIVE here.
                // --
                // Core directive: YesNoToggle.
                // NgModel-enabled directive: YesNoToggleForNgModel.
                var YesNoToggle = require( "YesNoToggleForNgModel" );
                // Configure the App component definition.
                var AppComponent = ng.core
                    .Component({
                        selector: "my-app",
                        directives: [ YesNoToggle ],
                        // In this version, we're putting two instances of the YesNoToggle
                        // component in the view at the same time. The first one uses the
                        // two-way data binding syntax to update the value directly. The
                        // second one uses ngModel to bridge the gap between the value and
                        // the component inputs.
                        template:
                        `
                            <p>
                                Can I wheez the juice?
                            </p>
                            <!-- Uses native two-way data binding. -->
                            <yes-no-toggle
                                [(value)]="canWheezTheJuice"
                                yes="Yeah buddy &mdash; wheez the ju-uice!"
                                no="No &mdash; no wheezing the ju-uice!">
                            </yes-no-toggle>
                            <!-- Uses NG-MODEL two-way data binding. -->
                            <yes-no-toggle
                                [(ngModel)]="canWheezTheJuice"
                                yes="Yeah buddy &mdash; wheez the ju-uice!"
                                no="No &mdash; no wheezing the ju-uice!">
                            </yes-no-toggle>
                            <p>
                                Current value:
                                <strong
                                    class="indicator"
                                    [class.can-wheez]="canWheezTheJuice">
                                    {{ canWheezTheJuice }}
                                </strong>.
                            </p>
                            <p>
                                <a (click)="toggleExternally()">Toggle input</a>
                                outside of component.
                            </p>
                        `
                    })
                    .Class({
                        constructor: AppController
                    })
                ;
                return( AppComponent );
                // I control the App component.
                function AppController() {
                    var vm = this;
                    // I determine if it's OK to "wheez the juice!".
                    // --
                    // Pop-Culture Reference: https://www.youtube.com/watch?v=nPn6sqGUM5A
                    vm.canWheezTheJuice = true;
                    // Expose the public methods.
                    vm.handleValueChange = handleValueChange;
                    vm.toggleExternally = toggleExternally;
                    // ---
                    // PUBLIC METHODS.
                    // ---
                    // I handle the valueChange event emitted by the YesNoToggle component
                    // and update the inputs accordingly.
                    function handleValueChange( newValue ) {
                        vm.canWheezTheJuice = newValue;
                    }
                    // I toggle the flag externally to the YesNoToggle component in an
                    // effort to ensure that the component will synchronize with the
                    // state of its own inputs.
                    function toggleExternally() {
                        vm.canWheezTheJuice = ! vm.canWheezTheJuice;
                    }
                }
            }
        );
        // --------------------------------------------------------------------------- //
        // --------------------------------------------------------------------------- //
        // I provide a toggle component that renders "Yes" text or "No" text based
        // on the state of its input value. When the component is activated, it will
        // emit a "valueChange" event with what the value of the input WOULD HAVE BEEN
        // if the value were mutated internally.
        define(
            "YesNoToggle",
            function registerYesNoToggle() {
                // Configure the YesNoToggle component definition.
                var YesNoToggleComponent = ng.core
                    .Component({
                        selector: "yes-no-toggle",
                        inputs: [ "value", "yes", "no" ],
                        outputs: [ "valueChangeEvents: valueChange" ],
                        host: {
                            "(click)": "toggle()",
                            "[class.for-yes]": "value",
                            "[class.for-no]": "! value"
                        },
                        template:
                        `
                            <span *ngIf="value">{{ yes }}</span>
                            <span *ngIf="! value">{{ no }}</span>
                        `
                    })
                    .Class({
                        constructor: YesNoToggleController
                    })
                ;
                return( YesNoToggleComponent );
                // I control the YesNoToggle component.
                function YesNoToggleController() {
                    var vm = this;
                    // I am the event stream for the valueChange output.
                    vm.valueChangeEvents = new ng.core.EventEmitter();
                    // Expose the public methods.
                    vm.toggle = toggle;
                    // ---
                    // PUBLIC METHODS.
                    // ---
                    // I emit the value change event when the user clicks on the host.
                    function toggle() {
                        // Notice that we are emitting the value of the input as it would
                        // have been had we implemented the mutation. However, since we
                        // don't own the value, we can't mutate it - we can only announce
                        // that it maybe should be mutated.
                        vm.valueChangeEvents.emit( ! vm.value );
                    }
                }
            }
        );
        // --------------------------------------------------------------------------- //
        // --------------------------------------------------------------------------- //
        // I provide an ngModel-enabled version of the YesNoToggle.
        define(
            "YesNoToggleForNgModel",
            function registerYesNoToggleForNgModel() {
                var YesNoToggle = require( "YesNoToggle" );
                // When we use the ngModel directive, the ngModel controller needs to
                // know how to access and mutate data on the target component. To do
                // this, it needs a "Value Accessor" that implements an interface for
                // writing values and responding to changes. It checks for this accessor
                // instance in the multi-provider collection, "NG_VALUE_ACCESSOR". In our
                // case, we're going to use the EXISTING INSTANCE of our
                // "YesNoToggleForNgModelDirective" directive as the accessor. This means
                // that our directive will actually be playing double-duty, both as the
                // local provider of the accessor as well as the implementer of the
                // said accessor.
                // --
                // NOTE: We have to use a forwardRef() since the directive isn't actually
                // defined yet.
                var valueAccessorProvider = ng.core.provide(
                    ng.common.NG_VALUE_ACCESSOR,
                    {
                        useExisting: ng.core.forwardRef(
                            function resolveDIToken() {
                                return( YesNoToggleForNgModelDirective );
                            }
                        ),
                        multi: true
                    }
                );
                // NOTE: If we wanted to side-step the use of NG_VALUE_ACCESSOR, we could
                // have had our directive "require" the ngModel instance and then inject
                // itself into the ngModel by way of:
                // --
                // ngModel.valueAccessor = this;
                // --
                // However, I am not sure how I feel about this. To me, that approach
                // seems to work "by coincidence", and not by intent.
                // Configure the YesNoToggleForNgModel directive definition. Notice that
                // the selector here only selects on instances of the YesNoToggle
                // element that are also using ngModel.
                // --
                // NOTE: This directive is also a local provider of the valueAccessor
                // collection which is providing the value accessor for the ngModel
                // component (which, incidentally, is also this component instance).
                var YesNoToggleForNgModelDirective = ng.core
                    .Directive({
                        selector: "yes-no-toggle[ngModel]",
                        host: {
                            "(valueChange)": "handleValueChange( $event )"
                        },
                        providers: [ valueAccessorProvider ]
                    })
                    .Class({
                        constructor: YesNoToggleForNgModelController
                    })
                ;
                // Configure the constructor to require the local YesNoToggle instance.
                // We need it in order to bridge the gap between ngModel and the state
                // of the toggle.
                YesNoToggleForNgModelDirective.parameters = [
                    new ng.core.Inject( YesNoToggle )
                ];
                // Notice that we are returning TWO directives here - the core YesNoToggle
                // component and the ngModel-enabled directive that we just defined. This
                // way, the calling context doesn't have to explicitly include both
                // directives - just "this one", which will implicitly include both.
                return( [ YesNoToggle, YesNoToggleForNgModelDirective ] );
                // I control the YesNoToggleForNgModel directive.
                // --
                // NOTE: Since this controller is also acting as double-duty for the
                // valueAccessor, it is also implementing the value accessor interface.
                function YesNoToggleForNgModelController( yesNoToggle ) {
                    var vm = this;
                    var onChange = function noop() {};
                    // Expose the public methods.
                    vm.handleValueChange = handleValueChange;
                    vm.registerOnChange = registerOnChange; // Value accessor interface.
                    vm.registerOnTouched = registerOnTouched; // Value accessor interface.
                    vm.writeValue = writeValue; // Value accessor interface.
                    // ---
                    // PUBLIC METHODS.
                    // ---
                    // I handle the valueChange event coming out of the YesNoToggle
                    // component. Since ngModel doesn't know about this event, we have
                    // to bridge the gap.
                    function handleValueChange( newValue ) {
                        // When we invoke the onChange() value accessor method, ngModel
                        // already assumes that the DOM (Document Object Model) is in the
                        // correct state. As such, we have ensure that the YesNoToggle
                        // reflects the change that it just emitted.
                        yesNoToggle.value = newValue;
                        // Tell ngModel.
                        onChange( newValue );
                    }
                    // I register the onChange handler provided by ngModel.
                    function registerOnChange( newOnChange ) {
                        onChange = newOnChange;
                    }
                    // I register the onTouched handler provided by ngModel.
                    function registerOnTouched() {
                        // console.log( "registerOnTouched" );
                    }
                    // I implement the value input invoked by ngModel. When ngModel wants
                    // to update the value of the target component, it doesn't know what
                    // property to use. As such, we have to bridge the gap between ngModel
                    // and the input property of the YesNoToggle component.
                    function writeValue( newValue ) {
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        setTimeout(
                            function avoidExpressionChangedAfterItHasBeenCheckedException() {
                                yesNoToggle.value = !! newValue; // Cast to boolean.
                            }
                        );
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // -- GROSS CODE SMELL. SOMETHING HERE IS TERRIBLY WRONG. ---- //
                        // CAUTION: If we don't use the setTimeout() method here, we get
                        // the following Angular error:
                        // --
                        // Expression 'value ...' has changed after it was checked.
                        // --
                        // I do not understand this, but Google shows me that this is a
                        // common problem. Hopefully one day, when I actually understand
                        // how change detection works in Angular 2, I won't need this.
                        // --
                        // NOTE: Enabling PROD mode is NOT A FIX (see note at top).
                    }
                }
            }
        );
    </script>
</body>
</html>
Output

You can jump to the latest bin by adding /latest to your URL

Dismiss x
public
Bin info
mikekidderpro
0viewers