Created
July 21, 2020 11:44
Looking At Different Click-To-Edit Implementations In Angular 9.1.12
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
export interface Project { | |
id: string; | |
name: string; | |
} | |
@Component({ | |
selector: "app-root", | |
styleUrls: [ "./app.component.less" ], | |
template: | |
` | |
<app-approach-one [projects]="projects"></app-approach-one> | |
<app-approach-two [projects]="projects"></app-approach-two> | |
<app-approach-three [projects]="projects"></app-approach-three> | |
` | |
}) | |
export class AppComponent { | |
public projects: Project[] = [ | |
{ id: "p1", name: "My Groovy Project" }, | |
{ id: "p2", name: "Another Cool Project" }, | |
{ id: "p3", name: "Much Project, Such Wow" }, | |
{ id: "p4", name: "A Good Project" } | |
]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
// Import the application components and services. | |
import { Project } from "./app.component"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@Component({ | |
selector: "app-approach-one", | |
inputs: [ "projects" ], | |
styleUrls: [ "./approach-one.component.less" ], | |
template: | |
` | |
<h2> | |
Encapsulated Editing Approach | |
</h2> | |
<ul> | |
<li *ngFor="let project of projects"> | |
<app-editable | |
[value]="project.name" | |
(valueChange)="saveProjectName( project, $event )"> | |
</app-editable> | |
</li> | |
</ul> | |
` | |
}) | |
export class ApproachOneComponent { | |
public projects!: Project[]; | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I handle the rename event, persisting the new value to the given project. | |
public saveProjectName( project: Project, newName: string ) : void { | |
// CAUTION: Normally, I would emit some sort of "rename" event to the calling | |
// context. But, for the sake of simplicity, I'm just mutating the project | |
// directly since having several sibling components that both edit project names | |
// is incidental and not the focus of this exploration. | |
project.name = newName; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
import { EventEmitter } from "@angular/core"; | |
// Import the application components and services. | |
import { Project } from "./app.component"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@Component({ | |
selector: "app-approach-three", | |
inputs: [ "projects" ], | |
styleUrls: [ "./approach-three.component.less" ], | |
template: | |
` | |
<h2> | |
Mixed Editing Approach | |
</h2> | |
<ul> | |
<li | |
*ngFor="let project of projects" | |
[ngSwitch]="( project === selectedProject )"> | |
<app-approach-three-editor | |
*ngSwitchCase="true" | |
[value]="project.name" | |
(valueChange)="saveProjectName( project, $event )" | |
(cancel)="cancel()"> | |
</app-approach-three-editor> | |
<div *ngSwitchCase="false" (click)="edit( project )"> | |
{{ project.name }} | |
</div> | |
</li> | |
</ul> | |
` | |
}) | |
export class ApproachThreeComponent { | |
public projects!: Project[]; | |
public selectedProject: Project | null; | |
// I initialize the approach-three component. | |
constructor() { | |
this.selectedProject = null; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I cancel editing of the selected project. | |
public cancel() : void { | |
this.selectedProject = null; | |
} | |
// I enable editing of the given project. | |
public edit( project: Project ) : void { | |
this.selectedProject = project; | |
} | |
// I handle the rename event, persisting the new value to the given project. | |
public saveProjectName( project: Project, newName: string ) : void { | |
// CAUTION: Normally, I would emit some sort of "rename" event to the calling | |
// context. But, for the sake of simplicity, I'm just mutating the project | |
// directly since having several sibling components that both edit project names | |
// is incidental and not the focus of this exploration. | |
project.name = newName; | |
this.selectedProject = null; | |
} | |
} | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
// FOR THE SAKE OF THE DEMO I'm keeping this component in the same file as the approach | |
// three component above in order to drive-home the intention that they are coupled | |
// together with intent. In reality, this component would be in a sibling file. | |
@Component({ | |
selector: "app-approach-three-editor", | |
inputs: [ "value" ], | |
outputs: [ | |
"cancelEvents: cancel", | |
"valueChangeEvents: valueChange" | |
], | |
styleUrls: [ "./approach-three-editor.component.less" ], | |
template: | |
` | |
<input | |
type="text" | |
name="value" | |
autofocus | |
[(ngModel)]="pendingValue" | |
(keydown.Enter)="processChanges()" | |
(keydown.Meta.Enter)="processChanges()" | |
(keydown.Escape)="cancel()" | |
/> | |
<button (click)="processChanges()"> | |
Save | |
</button> | |
<a | |
(click)="cancel()" | |
(keydown.Enter)="cancel()" | |
tabindex="0"> | |
Cancel | |
</a> | |
` | |
}) | |
export class ApproachThreeEditorComponent { | |
public cancelEvents: EventEmitter<void>; | |
public pendingValue: string; | |
public value!: string; | |
public valueChangeEvents: EventEmitter<string>; | |
// I initialize the approach-three editable component. | |
constructor() { | |
this.cancelEvents = new EventEmitter(); | |
this.pendingValue = ""; | |
this.valueChangeEvents = new EventEmitter(); | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I cancel the editing of the value. | |
public cancel() : void { | |
this.cancelEvents.emit(); | |
} | |
// I get called after the inputs are bound for the first time. | |
public ngOnInit() : void { | |
this.pendingValue = this.value; | |
} | |
// I process changes to the pending value. | |
public processChanges() : void { | |
// If the value hasn't changed, treat it like a cancel action. | |
if ( this.pendingValue === this.value ) { | |
this.cancelEvents.emit(); | |
} else { | |
this.valueChangeEvents.emit( this.pendingValue ); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
// Import the application components and services. | |
import { Project } from "./app.component"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@Component({ | |
selector: "app-approach-two", | |
inputs: [ "projects" ], | |
styleUrls: [ "./approach-two.component.less" ], | |
template: | |
` | |
<h2> | |
Inline Editing Approach | |
</h2> | |
<ul> | |
<li | |
*ngFor="let project of projects" | |
[ngSwitch]="( project === selectedProject )"> | |
<div *ngSwitchCase="true" class="editor"> | |
<input | |
type="text" | |
name="value" | |
autofocus | |
[(ngModel)]="pendingValue" | |
(keydown.Enter)="processChanges()" | |
(keydown.Meta.Enter)="processChanges()" | |
(keydown.Escape)="cancel()" | |
/> | |
<button (click)="processChanges()"> | |
Save | |
</button> | |
<a | |
(click)="cancel()" | |
(keydown.Enter)="cancel()" | |
tabindex="0"> | |
Cancel | |
</a> | |
</div> | |
<div *ngSwitchCase="false" (click)="edit( project )"> | |
{{ project.name }} | |
</div> | |
</li> | |
</ul> | |
` | |
}) | |
export class ApproachTwoComponent { | |
public pendingValue: string; | |
public projects!: Project[]; | |
public selectedProject: Project | null; | |
// I initialize the approach-two component. | |
constructor() { | |
this.pendingValue = ""; | |
this.selectedProject = null; | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I cancel editing of the selected project. | |
public cancel() : void { | |
this.selectedProject = null; | |
} | |
// I enable editing of the given project. | |
public edit( project: Project ) : void { | |
this.pendingValue = project.name; | |
this.selectedProject = project; | |
} | |
// I process changes to the selected project's name. | |
public processChanges() : void { | |
if ( this.pendingValue !== this.selectedProject!.name ) { | |
// CAUTION: Normally, I would emit some sort of "rename" event to the calling | |
// context. But, for the sake of simplicity, I'm just mutating the project | |
// directly since having several sibling components that both edit project | |
// names is incidental and not the focus of this exploration. | |
this.selectedProject!.name = this.pendingValue; | |
} | |
this.selectedProject = null; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import the core angular services. | |
import { Component } from "@angular/core"; | |
import { EventEmitter } from "@angular/core"; | |
// ----------------------------------------------------------------------------------- // | |
// ----------------------------------------------------------------------------------- // | |
@Component({ | |
selector: "app-editable", | |
inputs: [ "value" ], | |
outputs: [ "valueChangeEvents: valueChange" ], | |
styleUrls: [ "./editable.component.less" ], | |
template: | |
` | |
<div *ngIf="isEditing" class="editor"> | |
<input | |
type="text" | |
name="value" | |
autofocus | |
[(ngModel)]="pendingValue" | |
(keydown.Enter)="processChanges()" | |
(keydown.Meta.Enter)="processChanges()" | |
(keydown.Escape)="cancel()" | |
/> | |
<button (click)="processChanges()"> | |
Save | |
</button> | |
<a | |
(click)="cancel()" | |
(keydown.Enter)="cancel()" | |
tabindex="0"> | |
Cancel | |
</a> | |
</div> | |
<div *ngIf="( ! isEditing )" (click)="edit()"> | |
{{ value }} | |
</div> | |
` | |
}) | |
export class EditableComponent { | |
public isEditing: boolean; | |
public pendingValue: string; | |
public value!: string; | |
public valueChangeEvents: EventEmitter<string>; | |
// I initialize the editable component. | |
constructor() { | |
this.isEditing = false; | |
this.pendingValue = ""; | |
this.valueChangeEvents = new EventEmitter(); | |
} | |
// --- | |
// PUBLIC METHODS. | |
// --- | |
// I cancel the editing of the value. | |
public cancel() : void { | |
this.isEditing = false; | |
} | |
// I enable the editing of the value. | |
public edit() : void { | |
this.pendingValue = this.value; | |
this.isEditing = true; | |
} | |
// I process changes to the pending value. | |
public processChanges() : void { | |
// If the value actually changed, emit the change but don't change the local | |
// value - we don't want to break unidirectional data-flow. | |
if ( this.pendingValue !== this.value ) { | |
this.valueChangeEvents.emit( this.pendingValue ); | |
} | |
this.isEditing = false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment