Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.

docs(cookbook - custom form controls) Add cookbook for custom form controls with ngModel. #1432

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions public/docs/_examples/cb-custom-form-controls/e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/// <reference path='../_protractor/e2e.d.ts' />
'use strict';
describe('Building Custom Form Controls With ngModel', function () {

beforeAll(function () {
browser.get('');
});

it('should all be off', function () {
const toggles = element.all(by.css('cb-toggle'));

expect(toggles.getAttribute('class')).toContain('for-off');
expect(toggles.getAttribute('class')).not.toContain('for-on');
});

it('should all toggle on', function () {
const toggle = element.all(by.css('cb-toggle')).first();

toggle.click();

const toggles = element.all(by.css('cb-toggle'));

expect(toggles.getAttribute('class')).toContain('for-on');
expect(toggles.getAttribute('class')).not.toContain('for-off');
});

it('should use ng-form classes that reflect interactions', function () {
const toggle = element.all(by.css('cb-toggle')).last();

expect(toggle.getAttribute('class')).toContain('ng-pristine');
expect(toggle.getAttribute('class')).not.toContain('ng-dirty');

toggle.click();

expect(toggle.getAttribute('class')).toContain('ng-dirty');
expect(toggle.getAttribute('class')).not.toContain('ng-pristine');

const toggles = element.all(by.css('cb-toggle'));

expect(toggles.getAttribute('class')).toContain('for-off');
expect(toggles.getAttribute('class')).not.toContain('for-on');
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm-debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// #docregion
import { Component } from '@angular/core';
// #docregion providers
import { ToggleComponent } from './toggle.component';
import { ToggleNgModelDirective } from './toggle-ng-model.directive';

@Component({
selector: 'cb-app',
directives: [ ToggleComponent, ToggleNgModelDirective ],
template: `
<cb-toggle [value]="isOn" (valueChange)="handleChange($event)"></cb-toggle>
<cb-toggle [(value)]="isOn"></cb-toggle>
<cb-toggle [(ngModel)]="isOn"></cb-toggle>

<form #toggleForm="ngForm">
<cb-toggle #toggle="ngForm" ngControl="toggle" [(ngModel)]="isOn"></cb-toggle>
<p>
<strong>Form is Dirty:</strong> {{ toggleForm.dirty }}<br />
<strong>Toggle is Dirty:</strong> {{ toggle.dirty }}
</p>
</form>
`
})
// #enddocregion providers
export class AppComponent {
public isOn = false;

public handleChange(newValue: boolean): void {
this.isOn = newValue;
}
}
13 changes: 13 additions & 0 deletions public/docs/_examples/cb-custom-form-controls/ts/app/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// #docregion
import { bootstrap } from '@angular/platform-browser-dynamic';
import { AppComponent } from './app.component';

bootstrap(AppComponent).then(
function handleResolve() {
window.console.info( 'Angular finished bootstrapping your application!' )
},
function handleReject( error ) {
console.warn( 'Angular was not able to bootstrap your application.' );
console.error( error );
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// #docregion
import { ChangeDetectorRef, Directive, forwardRef, provide, SimpleChange } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/common';

// #docregion valueaccesor
import { ToggleComponent } from './toggle.component';

// #docregion provider
@Directive({
selector: 'cb-toggle[ngControl],cb-toggle[ngFormControl],cb-toggle[ngModel]',
providers: [
provide(
NG_VALUE_ACCESSOR,
{
useExisting: forwardRef(() => ToggleNgModelDirective),
multi: true
}
)
],
host: {
'(valueChange)': 'handleValueChange($event)'
}
})
export class ToggleNgModelDirective implements ControlValueAccessor {
// #enddocregion provider

private changeCount = 0;
private onChange = noop;
private onTouched = noop;

constructor(private toggle: ToggleComponent, private changeDetectorRef: ChangeDetectorRef) {}

handleValueChange(newValue: boolean): void {
// When implementing ngModel, we are purposefully circumventing one-way data flow.
// As such, when the target component emits a change event, we want to turn around
// and pipe that change right back into the target component.
this.applyChangeToTarget(this.toggle.value, newValue);
this.onChange(newValue);
}

registerOnChange(newOnChange: any): void {
this.onChange = newOnChange;
}

registerOnTouched(newOnTouched: any): void {
// CAUTION: For this demo, we are not worrying about "touch" events.
this.onTouched = newOnTouched;
}

writeValue(newValue: any): void {
this.applyChangeToTarget(this.toggle.value, !! newValue );
}

// #docregion applychanges
private applyChangeToTarget(previousValue: boolean, currentValue: boolean): void {
// Pipe the value into the target and alert the change detector that inputs
// have been changed PROGRAMMATICALLY.
this.toggle.value = currentValue;
this.changeDetectorRef.markForCheck();

// Unfortunately, when we change the target component's inputs programmatically,
// Angular doesn't help us with the life-cycle methods. As such, we have to fill in
// the ngOnChanges() gap as the target component may be depending on it internally.
if (this.toggle.ngOnChanges) {
let change = new SimpleChange( previousValue, currentValue );

// Unfortunately, Angular uses a private token internally to determine if the
// given change is the "first" change (for the given value) in the component's
// life-cycle. Since the target component may be relying on the isFirstChange()
// method to work as expected, we have to patch it manually.
if (!this.changeCount++) {
change.isFirstChange = (() => true);
}

this.toggle.ngOnChanges({ value: change });
}
}
// #enddocregion applychanges

}
// #enddocregion valueaccesor

function noop(): void {
// No-operation function.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// #docregion
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';

// #docregion component
@Component({
selector: 'cb-toggle',
host: {
'[class.for-on]': 'value',
'[class.for-off]': '! value',
'(click)': 'handleClick($event)'
},
template: `
<div class='switch'>
<span class='thumb'></span>
</div>
<span class='label'>{{ ( value ? 'On' : 'Off' ) }}</span>
`
})
export class ToggleComponent implements OnChanges {
@Input() public value = false;
@Output() public valueChange = new EventEmitter<boolean>;

handleClick(event: any): void {
this.valueChange.next( ! this.value );
}

ngOnChanges(changes: any): void {
console.log(
'Toggle changed from %s to %s during %s change.',
changes.value.previousValue,
changes.value.currentValue,
(changes.value.isFirstChange() ? 'first' : 'subsequent')
);
}

}
// #enddocregion component
41 changes: 41 additions & 0 deletions public/docs/_examples/cb-custom-form-controls/ts/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="/">

<title>
Creating Custom Form Controls Using ngModel
</title>

<!-- #docregion style -->
<link rel="stylesheet" type="text/css" href="styles.css">
<link rel="stylesheet" type="text/css" href="sample.css">
<!-- #enddocregion style -->

<!-- Polyfill(s) for older browsers -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>

<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>

<script src="systemjs.config.js"></script>
<script>
System.import('app')
.then(function() { console.info( "System.js loaded your application module." )})
.catch(function(err){ console.error(err); });
</script>
</head>
<body>

<h1>
Creating Custom Form Controls Using ngModel
</h1>

<cb-app>Loading app...</cb-app>

</body>
</html>
9 changes: 9 additions & 0 deletions public/docs/_examples/cb-custom-form-controls/ts/plnkr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"description": "Creating Custom Form Controls Using ngModel",
"files": [
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1].*"
],
"tags": [ "cookbook" ]
}
61 changes: 61 additions & 0 deletions public/docs/_examples/cb-custom-form-controls/ts/sample.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* #docregion */
cb-toggle {
cursor: pointer ;
display: table ;
height: 26px ;
line-height: 27px ;
margin-bottom: 16px ;
user-select: none ;
-moz-user-select: none ;
-webkit-user-select: none ;
}

cb-toggle div.switch {
background-color: #333333 ;
border-radius: 26px 26px 26px 26px ;
display: inline-block ;
height: 26px ;
overflow: hidden ;
position: relative ;
width: 55px ;
}

cb-toggle span.thumb {
background-color: #F0F0F0 ;
border-radius: 100px ;
height: 20px ;
left: 0px ;
margin-left: 4px ;
position: absolute ;
transition: all 200ms ease ;
-moz-transition: all 200ms ease ;
-webkit-transition: all 200ms ease ;
top: 3px ;
width: 20px ;
}

cb-toggle span.label {
display: inline-block ;
margin-left: 7px ;
text-transform: uppercase ;
vertical-align: top ;
}

cb-toggle.for-on div.switch {
background-color: #30659B ;
}

cb-toggle.for-on span.thumb {
left: 100% ;
margin-left: -24px ;
}

form {
border: 1px dashed #CCCCCC ;
border-radius: 4px 4px 4px 4px ;
padding: 20px 20px 20px 20px ;
}

form p {
margin: 0px 0px 0px 0px ;
}
5 changes: 5 additions & 0 deletions public/docs/ts/latest/cookbook/_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
"intro": "Use relative URLs for component templates and styles."
},

"custom-form-controls": {
"title": "Custom Form Controls",
"intro": "Creating custom form controls using ngModel."
},

"dependency-injection": {
"title": "Dependency Injection",
"intro": "Techniques for Dependency Injection"
Expand Down
Loading