While working on my last Angular project. I can’t help but notice how unease I felt with lots of HTML input tags spanning through the component template code I was working on. “There should be a more neat way to abstract these input tags”, I murmured.
Afterward, I decided to draft some syntax of my idea of what the abstraction should look like. Aha! What could be the perfect tool for the job? Pipes, holy smokes yes! pipes.
Angular pipes provide a very sleek abstraction, a right tool for the abstraction quest. So I came up with the pipe syntax below, neat uh?
Quick Disclaimer: I was just trying to experiment.
Now we have a valid pipe syntax that contains sufficient parameters needed to create an input tag. It’s time we write a pipe that transforms our syntax to a pseudo HTML tag. The pipe will return a result which contains an input tag syntax wrapped in double square brackets.
Here is what the code pipe code looks like. pipeform.pipe.ts
import {
Pipe,
PipeTransform
} from '@angular/core';
@Pipe({
name: 'PipeForm'
})
export class FormPipe implements PipeTransform {
// transforms the pipe input and returns a string following this format `[[<input/>]]`
transform(elem: string, type: string, options ? : object): string {
// declare output container..
let out: string;
// switch between the type if element we want to create
switch (elem) {
case 'input':
// case input tag,
out = `[[<input type="${type}"`;
// loop through the options parameter and format it into the out variable like HTML attributes.
Object.entries(options).forEach((value: string[]) => {
out += ` ${value[0]}="${value[1]}"`;
});
break;
}
// append the final, ending string.
out += '/>]]';
// we done here ;-)
return out;
}
}
Yes! That works but just returns a dummy string right? Finally, I then realized that I needed some sort of container which I can then use to parse the result returned by the pipe and create the actual input elements then inject them into the DOM using Renderer2. So I needed to update my initial syntax. To use pipe-forms, you have to wrap it inside the ngx-pipeform component which will act as a renderer for the pipe-form. So the updated syntax looks like👇
Now let’s create the component that will serve as a wrapper for pipe-forms. Here is a brief on how it works.
- It takes the result returned by pipeform pipe, then parses it.
- Creates the input tag elements and then injects them into the DOM.
Here is what the code looks like. pipeform.component.ts
import {
Component,
ViewContainerRef,
AfterViewInit,
Renderer2
} from '@angular/core';
@Component({
selector: 'ngx-pipeform',
template: `<ng-content></ng-content>`,
})
export class PipeformComponent implements AfterViewInit {
constructor(private viewRef: ViewContainerRef, private rd: Renderer2) {}
ngAfterViewInit(): void {
// after view init, lets get things done..
// filter node type of text..
// if text matches pipeform syntax, replace it with the input tag
// the create the element and inject it into the dom with Renderer2.
// lets travel through the DOM..
this.recurseDomChildren(this.viewRef.element.nativeElement);
}
recurseDomChildren(start) {
let nodes;
if (start.childNodes) {
nodes = start.childNodes;
this.loopNodeChildren(nodes);
}
}
loopNodeChildren(nodes) {
let node;
for (let i = 0; i < nodes.length; i++) {
node = nodes[i];
// try to parse each node..
this.pipeFormParse(node);
if (node.childNodes) {
this.recurseDomChildren(node);
}
}
}
pipeFormParse(node) {
// if the content of this node is a text node
if (node.nodeType === 3) {
// get its text content
const textContent = node.textContent;
// match the occurence of the pipe-form syntax, if found return an array of the result.
const pipeForms = textContent.match(/\[\[(.*?)]]/gi);
if (pipeForms) {
// strip the double square brackets from all of the results.
const readyElements = pipeForms.map(item => item.split('[[')[1].split(']]')[0]);
// create a div container with Renderer2
let elem = this.rd.createElement('div');
// insert the prepaired input tag into the div.
elem.innerHTML = readyElements.join(' ');
// replace this current node with the new div node we just created.
node.parentElement.replaceChild(elem, node);
}
}
}
}
Cheers! It works 😂🍻, but there are still a lot of improvements, questions, and comments we need to go over. I will leave it for you to decide.
Example hosted on Stackblitz
Live demo on Stackblitz - https://angular-pipe-form.stackblitz.io/ Feel free to check the source code https://stackblitz.com/edit/angular-pipe-form
My Questions
- It works, but does it worth the effort?
- What does it cost, I mean performance in the production environment?
- Does the syntax look a bit nicer or cleaner?
Improvements
- Styling the pipe-forms.
- Making it work with Angular Models, I haven’t attempted this.
- Form validations, please!
- Maybe a nice optimization of the DOM traversing logics.
Final Verdict
Very well 😉, My aim here is just to see if we can use pipes to create forms. Yes, we can! But is it a good approach? Isn’t this out of context of what pipes meant for? Amigo, I don’t know, was just trying out new ways to get things done. Express your thoughts in words as comments below.
Till next time! Peace out.💜
Top comments (2)
"Whoa" is the first thing that comes to mind when I see pipes used to create the DOM in Angular, never thought about taking that approach at all haha! Idk how efficient it is to take this approach, or if there are massive pitfalls to this approach either.
Have you looked into how this approach compares to the more traditional approach of using a directive (specifically a structural directive) to modify the DOM?
No, not at all, was just trying to experiment. Will have a deeper look into this, but what I do think is that pipes might give a lot of syntactic freedom and abstraction. Imagine using this approach to create some sort of pipe-elements