When creating features we often need to create instances based on parameters coming from an endpoint or a similar dynamic source. In this article I'll try to give you an idea on how we can use the single factory pattern to approach such cases.
We'll refactor a piece of code to make it more scalable and understandable, Let's suppose we have this premise:
Given an object of element's data, write an algorithm that allows users to provide a type of element to render it dynamically. Also, please validate the elements as required:
- Generic input text fields such as Email and Password should be validated.
- Make sure line breaks are stripped off textarea elements.
Example of a source of data, let's call it elementsData.js:
export default {
elements: {
email: {
type: 'email',
text: 'Email',
name: 'userEmail'
},
summary: {
type: 'textarea',
text: 'Summary',
name: 'summary'
},
role: {
type: 'select',
text: 'Role',
name: 'role',
options: [
{
value: 1,
display: 'Software Developer'
},
{
value: 2,
display: 'Designer'
},
{
value: 3,
display: 'Manager'
},
...
]
},
...
}
};
A non scalable approach
Now I'll write what could be a pseudo "solution" to dynamically create the types of form elements and validate them (Note that I'll only define the methods that matter for the purpose of this article):
import config from './elementsData';
export default class FormElement {
constructor(type) {
this.type = type;
this.elements = config.elements;
}
getElementByType() {
return this.type in this.elements ? this.elements[this.type] : null;
}
/* This would validate our email*/
emailValidator() { ... }
/* this would remove line breaks from our textareas */
textareaSanitizer() { ... }
/* We would use this to bind all the validators and sanitizers events */
bindEventListeners() { ... }
renderFormElement() {
const element = this.getElementByType();
if (!element) {
return false;
}
switch(this.type) {
case 'email':
return `
<div class="field-wrapper">
<input type="email" name=${element.name} placeholder=${element.text} />
</div>
`;
break;
case: 'textarea':
return `
<div class="field-wrapper">
<textarea name=${element.name} placeholder=${element.text} />
</div>
`;
case 'select':
return `
<div class="field-wrapper">
<select name=${element.name}>
${element.options.map(option => `
<option value=${option.value}>
${option.display}
</option>
`)}
</select>
</div>
`;
}
}
}
and we would instanciate the class in our main.js like:
const formElement = new FormElement('email');
formElement.renderFormElement();
This should work, right?, we're consuming the data, dynamically creating elements and validating them... BUT, there are some things we're not seeing, I want you to think in the future, what would happen with this class when you or someone else needs to add more and more form elements?, the renderFormElements
method will grow and we'll end up having a huge method with endless conditions, validation methods, and let's not even talk about the complexity and scalability.
Implementing Single Factory
The factory pattern is a design pattern that's part of the creational group, it basically deals with the issue of creating objects when the class that instanciates it needs to be dynamic, it also helps a lot on organizing your code, because:
- Isolates the objects that need to be created.
- Promotes small classes with less responsabilities.
- Delegates the responsability of object creation to a class called "factory".
- Creates the instances by receiving the dynamic value in your entry point.
Here is a visual representation I created to demonstrate how the factory works.
Now we'll start by refactoring our "solution" based on the list we've created above.
Isolate the objects to keep single responsabilities
The form elements select, email, textarea can be easily isolated by moving the logic that involves them to a folder called /FormElements
or /FormElementTypes
(you can give any name that makes sense):
/FormElements/email.js
export default class Email {
constructor(element) {
this.name = element.name;
this.text = element.text;
}
bindEmailValidator() { ... }
emailValidator() { ... }
render() {
return `
<div class="email-wrapper">
<input type="email" name=${this.name} placeholder=${this.text} />
</div>
`;
}
}
Notice that we're moving the validation and binding methods to the element's class, we would do the same for the other elements (textarea, select, ...). This will allow us to scale and keep the logic for each type of element isolated.
Delegate the responsability of object creation to a class called "factory"
The factory does a couple of things:
- Imports the types of elements.
- Creates an
ELEMENTS
object with the types of elements available. - Creates a static method to be able to create instances directly by using the class name.
- Dynamically instanciates based on the type passed.
Below we have the code representing this:
factories/elementsFactory.js
import Email from './FormElements/email';
import Textarea from './FormElements/textarea';
import Select from './FormElements/select';
const ELEMENTS = {
Email,
Textarea,
Select
};
export default class ElementsFactory {
static createInstance(data) {
const elementCreator = ELEMENTS[data.type];
const element = elementCreator ? new elementCreator(data) : null;
return element;
}
}
Creates the instances by receiving the dynamic value in your entry point (our main.js in this case).
Nothing complex to explain here, we're just validating that the argument passed exists in our object of elements, if it does then we create an instance by using the factory method createInstance
, we pass down the data needed and render the element with the render
method, below you can see the code:
main.js
import ElementsFactory as Factory from './factories/FormElementsFactory';
import config from './elementsData';
function Init() {
/* This could be received dynamically */
const dynamicElement = 'email';
if (!(dynamicElement in config.elements)) {
return false;
}
const element = Factory.createInstance(config.elements[dynamicElement]);
element.render();
}
Init();
To finish, here is a representation of our folder structure after refactoring
├── main.js
├── factories/
│ ├── ElementsFactory.js
└── formElements/
├── email.js
├── select.js
└── textarea.js
Cool, right? Now every time you want to add a new element, it's just a matter of adding it to the /formElements
folder and importing it in our factory so it can be instantiated, Also, if you want to remove an element it's just a matter of deleting the import line and the file from the /formElements
folder.
Ok, I think that's it for this article guys, hope you were able to understand a little more about the factory pattern, if you did remember to share it on twitter or facebook, you can also subscribe to our email list below.
See you in the next one!
Top comments (1)
Nice post. This is an elegant solution for the dynamic form fields creation