<html>
<head>
<meta charset="utf-8" />
<title>
FAILURE: Using ngModel With A Custom Component In Angular 2 Beta 2
</title>
<style>
a {
color: red ;
cursor: pointer ;
text-decoration: underline ;
user-select: none ;
user-select: none ;
user-select: none ;
}
yes-no-toggle {
background-color: #FAFAFA ;
border: 2px solid #CCCCCC ;
border-radius: 5px 5px 5px 5px ;
cursor: pointer ;
display: table ;
margin: 16px 0px 16px 0px ;
padding: 15px 23px 15px 23px ;
user-select: none ;
user-select: none ;
user-select: none ;
}
yes-no-toggle.for-no {
border-color: red ;
color: red ;
}
yes-no-toggle.for-yes {
border-color: green ;
color: green ;
}
strong.indicator {
color: red ;
}
strong.indicator.can-wheez {
color: green ;
}
</style>
</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 — wheez the ju-uice!"
no="No — no wheezing the ju-uice!">
</yes-no-toggle>
<!-- Uses NG-MODEL two-way data binding. -->
<yes-no-toggle
[(ngModel)]="canWheezTheJuice"
yes="Yeah buddy — wheez the ju-uice!"
no="No — 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
300px
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |