Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created July 21, 2020 11:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/01719a67fd8fe0b1c5d9fab1b7a604de to your computer and use it in GitHub Desktop.
Save bennadel/01719a67fd8fe0b1c5d9fab1b7a604de to your computer and use it in GitHub Desktop.
Looking At Different Click-To-Edit Implementations In Angular 9.1.12
// 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" }
];
}
// 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;
}
}
// 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 );
}
}
}
// 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;
}
}
// 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