Extensão via Prototype
Considere um cenário onde precisamos repetir uma string um número determinado de vezes:
'abc'.replicate(3) // abcabcabc
Uma abordagem comum é vincular o método diretamente ao protótipo de String:
String.prototype.replicate = String.prototype.replicate || function(count) {
let output = '';
for (let idx = 0; idx < count; idx++) {
output += this;
}
return output;
};
Plugins jQuery
Existem três abordagens principais para criar plugins jQuery:
$.extend()— adiciona métodos estáticos ao namespace jQuery$.fn— adiciona novos métodos encadeáveis a elementos DOM$.widget()— utiliza a fábrica de widgets do jQuery UI para componentes mais robustos
A primiera abordagem é simples e cria funções utilitárias acessíveis diretamente via $:
$.extend({
greet: function(name) {
console.log('Olá, ' + (name || 'Visitante') + '!');
}
});
$.greet();
$.greet('Maria');
Para um plugin orientado a objetos mais estruturado:
const Styler = function(target, config) {
this.$target = target;
this.defaults = {
color: 'red',
fontSize: '12px',
textDecoration: 'none'
};
this.settings = $.extend({}, this.defaults, config);
};
Styler.prototype = {
apply: function() {
return this.$target.css({
color: this.settings.color,
fontSize: this.settings.fontSize,
textDecoration: this.settings.textDecoration
});
}
};
$.fn.styleEnhancer = function(options) {
const instance = new Styler(this, options);
return instance.apply();
};
Uso:
$(function() {
$('a').styleEnhancer({
color: '#2C9929',
fontSize: '20px'
});
});
Plugins Vue.js
Plugins Vue podem adicionar funcionalidades globais de diversas formas:
- Métodos ou propriedades globais
- Recursos globais como diretivas, filtros e transições
- Opções de componentes via mixin
- Métodos de instância em
Vue.prototype - Bibliotecas com API própria combinando os recursos acima
Todo plugin Vue deve expor um método install:
const CustomPlugin = {
install(Vue, options) {
// Método global
Vue.globalHelper = function() {
// lógica...
};
// Diretiva global
Vue.directive('my-directive', {
bind(el, binding) {
// lógica...
}
});
// Mixin global
Vue.mixin({
created() {
// lógica...
}
});
// Método de instância
Vue.prototype.$utility = function(config) {
// lógica...
};
}
};
export default CustomPlugin;
Ativação do plugin:
Vue.use(CustomPlugin);
Vue.use(CustomPlugin, { someOption: true });
Construindo um Sistema de Validação de Formulários Extensível
O princípio central é seguir o padrão Strategy, mantendo o sistema aberto para extensão e fechado para modificação.
Estrutura HTML Base
<form id="registerForm" method="post" onsubmit="return handleSubmit()">
<p>
<label>Nome de usuário:</label>
<input type="text" name="userName" />
</p>
<p>
<label>Senha:</label>
<input type="text" name="password" />
</p>
<p>
<label>Telefone:</label>
<input type="text" name="phoneNumber" />
</p>
<div>
<button type="submit">Enviar</button>
</div>
</form>
Implementação Inicial
function handleSubmit() {
const form = document.getElementById("registerForm");
const fieldConfigs = [
{
value: form.userName.value,
constraints: [
{ type: 'required', msg: 'Nome de usuário é obrigatório' },
{ type: 'minLength:3', msg: 'Nome deve ter no mínimo 3 caracteres' }
]
},
{
value: form.password.value,
constraints: [
{ type: 'required', msg: 'Senha é obrigatória' },
{ type: 'minLength:6', msg: 'Senha deve ter no mínimo 6 caracteres' }
]
},
{
value: form.phoneNumber.value,
constraints: [
{ type: 'required', msg: 'Telefone é obrigatório' },
{ type: 'isMobile', msg: 'Formato de telefone inválido' }
]
}
];
const error = validator.run(fieldConfigs);
if (error) {
alert(error);
return false;
}
return true;
}
const validator = (function() {
const strategies = {
required: function(input, msg) {
if (!input) return msg;
},
minLength: function(input, min, msg) {
if (String(input).length < min) return msg;
},
maxLength: function(input, max, msg) {
if (String(input).length > max) return msg;
},
isMobile: function(input, msg) {
if (!/^1\d{10}$/.test(input)) return msg;
},
isTel: function(input, msg) {
if (!/^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/.test(input)) return msg;
}
};
return {
run(configs) {
for (let i = 0; i < configs.length; i++) {
if (configs[i].value === undefined) {
return 'Campo não encontrado';
}
for (let j = 0; j < configs[i].constraints.length; j++) {
const c = configs[i].constraints[j];
const parts = c.type.split(':');
const name = parts.shift();
parts.unshift(configs[i].value);
parts.push(c.msg);
const result = strategies[name].apply(null, parts);
if (result) return result;
}
}
}
};
})();
Tornando o Sistema Extensível
O problema da abordagem acima é que regras específicas não podem ser adicionadas sem modificar o objeto strategies. A solução é expor um método de registro:
const validator = (function() {
const strategies = {
required: function(input, msg) {
if (!input) return msg;
},
minLength: function(input, min, msg) {
if (String(input).length < min) return msg;
},
maxLength: function(input, max, msg) {
if (String(input).length > max) return msg;
},
isMobile: function(input, msg) {
if (!/^1\d{10}$/.test(input)) return msg;
},
isTel: function(input, msg) {
if (!/^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/.test(input)) return msg;
}
};
return {
run(configs) {
for (let i = 0; i < configs.length; i++) {
if (configs[i].value === undefined) {
return 'Campo não encontrado';
}
for (let j = 0; j < configs[i].constraints.length; j++) {
const c = configs[i].constraints[j];
const parts = c.type.split(':');
const name = parts.shift();
parts.unshift(configs[i].value);
parts.push(c.msg);
const result = strategies[name].apply(null, parts);
if (result) return result;
}
}
},
register(name, fn) {
strategies[name] = fn;
}
};
})();
Registrando uma regra personalizada para validar apenas caracteres alfanuméricos:
validator.register('isAlphaNum', function(input, msg) {
if (/[^a-zA-Z0-9]/.test(input)) return msg;
});
function handleSubmit() {
const form = document.getElementById("registerForm");
validator.register('isAlphaNum', function(input, msg) {
if (/[^a-zA-Z0-9]/.test(input)) return msg;
});
const fieldConfigs = [
{
value: form.userName.value,
constraints: [
{ type: 'required', msg: 'Nome de usuário é obrigatório' },
{ type: 'minLength:3', msg: 'Nome deve ter no mínimo 3 caracteres' },
{ type: 'isAlphaNum', msg: 'Nome deve conter apenas letras e números' }
]
},
{
value: form.password.value,
constraints: [
{ type: 'required', msg: 'Senha é obrigatória' },
{ type: 'minLength:6', msg: 'Senha deve ter no mínimo 6 caracteres' }
]
},
{
value: form.phoneNumber.value,
constraints: [
{ type: 'required', msg: 'Telefone é obrigatório' },
{ type: 'isMobile', msg: 'Formato de telefone inválido' }
]
}
];
const error = validator.run(fieldConfigs);
if (error) {
alert(error);
return false;
}
return true;
}
Coletando Todos os Erros Simultaneamente
Para exibir múltiplas mensagens de erro ao mesmo tempo, adicionamos um método runAll:
const validator = (function() {
const strategies = {
required: function(input, msg) {
if (!input) return msg;
},
minLength: function(input, min, msg) {
if (String(input).length < min) return msg;
},
maxLength: function(input, max, msg) {
if (String(input).length > max) return msg;
},
isMobile: function(input, msg) {
if (!/^1\d{10}$/.test(input)) return msg;
},
isTel: function(input, msg) {
if (!/^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/.test(input)) return msg;
}
};
return {
run(configs) {
for (let i = 0; i < configs.length; i++) {
if (configs[i].value === undefined) return 'Campo não encontrado';
for (let j = 0; j < configs[i].constraints.length; j++) {
const c = configs[i].constraints[j];
const parts = c.type.split(':');
const name = parts.shift();
parts.unshift(configs[i].value);
parts.push(c.msg);
const result = strategies[name].apply(null, parts);
if (result) return result;
}
}
},
runAll(configs) {
const errors = [];
for (let i = 0; i < configs.length; i++) {
if (configs[i].value === undefined) return 'Campo não encontrado';
for (let j = 0; j < configs[i].constraints.length; j++) {
const c = configs[i].constraints[j];
const parts = c.type.split(':');
const name = parts.shift();
parts.unshift(configs[i].value);
parts.push(c.msg);
const result = strategies[name].apply(null, parts);
if (result) {
errors.push({
field: configs[i].alias,
rule: name,
message: result
});
break;
}
}
}
return errors.length > 0 ? errors : false;
},
register(name, fn) {
strategies[name] = fn;
}
};
})();
HTML ajustado com elementos para exibição de erro:
<form id="registerForm" method="post" onsubmit="return handleSubmit()">
<p>
<label>Nome de usuário:</label>
<input type="text" name="userName" />
<span class="error-msg"></span>
</p>
<p>
<label>Senha:</label>
<input type="text" name="password" />
<span class="error-msg"></span>
</p>
<p>
<label>Telefone:</label>
<input type="text" name="phoneNumber" />
<span class="error-msg"></span>
</p>
<div>
<button type="submit">Enviar</button>
</div>
</form>
<style>
.error-msg { color: red; }
</style>
function handleSubmit() {
const form = document.getElementById("registerForm");
validator.register('isAlphaNum', function(input, msg) {
if (/[^a-zA-Z0-9]/.test(input)) return msg;
});
const fieldConfigs = [
{
value: form.userName.value,
alias: 'userName',
constraints: [
{ type: 'required', msg: 'Nome de usuário é obrigatório' },
{ type: 'minLength:3', msg: 'Nome deve ter no mínimo 3 caracteres' },
{ type: 'isAlphaNum', msg: 'Nome deve conter apenas letras e números' }
]
},
{
value: form.password.value,
alias: 'password',
constraints: [
{ type: 'required', msg: 'Senha é obrigatória' },
{ type: 'minLength:6', msg: 'Senha deve ter no mínimo 6 caracteres' }
]
},
{
value: form.phoneNumber.value,
alias: 'phoneNumber',
constraints: [
{ type: 'required', msg: 'Telefone é obrigatório' },
{ type: 'isMobile', msg: 'Formato de telefone inválido' }
]
}
];
const errors = validator.runAll(fieldConfigs);
if (errors) {
errors.forEach(function(err) {
const inputEl = document.querySelector('#registerForm [name="' + err.field + '"]');
inputEl.nextElementSibling.innerHTML = err.message;
});
return false;
}
return true;
}
Validação em Tempo Real com Eventos de Blur
Para suportar validação ao perder o foco, adicionamos um método bindEvents:
const validator = (function() {
const strategies = {
required: function(input, msg) {
if (!input) return msg;
},
minLength: function(input, min, msg) {
if (String(input).length < min) return msg;
},
maxLength: function(input, max, msg) {
if (String(input).length > max) return msg;
},
isMobile: function(input, msg) {
if (!/^1\d{10}$/.test(input)) return msg;
},
isTel: function(input, msg) {
if (!/^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/.test(input)) return msg;
}
};
function executeRule(value, constraint) {
const parts = constraint.type.split(':');
const name = parts.shift();
parts.unshift(value);
parts.push(constraint.msg);
return strategies[name].apply(null, parts);
}
return {
run(ruleMap, configs) {
for (let i = 0; i < configs.length; i++) {
if (configs[i].value === undefined) return 'Campo não encontrado';
const rules = ruleMap[configs[i].alias];
for (let j = 0; j < rules.length; j++) {
const result = executeRule(configs[i].value, rules[j]);
if (result) return result;
}
}
},
runAll(ruleMap, configs) {
const errors = [];
for (let i = 0; i < configs.length; i++) {
if (configs[i].value === undefined) return 'Campo não encontrado';
const rules = ruleMap[configs[i].alias];
for (let j = 0; j < rules.length; j++) {
const result = executeRule(configs[i].value, rules[j]);
if (result) {
errors.push({
field: configs[i].alias,
message: result
});
break;
}
}
}
return errors.length > 0 ? errors : false;
},
bindEvents(config) {
const ruleEntries = config.rules;
for (const fieldName in ruleEntries) {
if (!ruleEntries.hasOwnProperty(fieldName)) continue;
const inputEl = document.querySelector(config.container + ' [name="' + fieldName + '"]');
const blurRules = ruleEntries[fieldName].filter(function(r) {
return r.trigger === 'blur';
});
(function(dom, domRules) {
dom.addEventListener('blur', function() {
const val = dom.value;
let foundError = '';
for (let k = 0; k < domRules.length; k++) {
const result = executeRule(val, domRules[k]);
if (result) {
foundError = domRules[k].msg;
break;
}
}
dom.nextElementSibling.innerHTML = foundError;
}, false);
})(inputEl, blurRules);
}
},
register(name, fn) {
strategies[name] = fn;
}
};
})();
validator.register('isAlphaNum', function(input, msg) {
if (/[^a-zA-Z0-9]/.test(input)) return msg;
});
const ruleMap = {
userName: [
{ type: 'required', msg: 'Nome de usuário é obrigatório', trigger: 'blur' },
{ type: 'minLength:3', msg: 'Nome deve ter no mínimo 3 caracteres', trigger: 'blur' },
{ type: 'isAlphaNum', msg: 'Nome deve conter apenas letras e números', trigger: 'blur' }
],
password: [
{ type: 'required', msg: 'Senha é obrigatória', trigger: 'blur' },
{ type: 'minLength:6', msg: 'Senha deve ter no mínimo 6 caracteres', trigger: 'blur' }
],
phoneNumber: [
{ type: 'required', msg: 'Telefone é obrigatório', trigger: 'blur' },
{ type: 'isMobile', msg: 'Formato de telefone inválido', trigger: 'blur' }
]
};
validator.bindEvents({
container: '#registerForm',
rules: ruleMap
});
function handleSubmit() {
const form = document.getElementById("registerForm");
const fieldConfigs = [
{ value: form.userName.value, alias: 'userName' },
{ value: form.password.value, alias: 'password' },
{ value: form.phoneNumber.value, alias: 'phoneNumber' }
];
const errors = validator.runAll(ruleMap, fieldConfigs);
if (errors) {
errors.forEach(function(err) {
const inputEl = document.querySelector('#registerForm [name="' + err.field + '"]');
inputEl.nextElementSibling.innerHTML = err.message;
});
return false;
}
return true;
}
HTML e CSS permanecem os mesmos da seção anterior, com os elementos <span class="error-msg"> ao lado de cada input.