Rule playground
Type a number in the widget and see what the Laravel rule on our server says, in English, Arabic or Urdu.
<main class="demo demo--wide">
<div class="controls demo-card" aria-label="Rule options">
<div class="demo-row"><span class="label">Countries</span>
<div class="demo-seg" role="radiogroup" aria-label="Allowed countries" data-key="countries">
<button type="button" role="radio" aria-checked="true" data-value="">Any</button>
<button type="button" role="radio" aria-checked="false" data-value="AE,SA">UAE, Saudi</button>
<button type="button" role="radio" aria-checked="false" data-value="PK">Pakistan</button>
</div>
</div>
<div class="demo-row"><span class="label">Type</span>
<div class="demo-seg" role="radiogroup" aria-label="Allowed types" data-key="types">
<button type="button" role="radio" aria-checked="true" data-value="">Any</button>
<button type="button" role="radio" aria-checked="false" data-value="mobile">Mobile</button>
<button type="button" role="radio" aria-checked="false" data-value="fixed">Landline</button>
</div>
</div>
<div class="demo-row"><span class="label">Language</span>
<div class="demo-seg" role="radiogroup" aria-label="Message language" data-key="locale">
<button type="button" role="radio" aria-checked="true" data-value="en">EN</button>
<button type="button" role="radio" aria-checked="false" data-value="ar">عربي</button>
<button type="button" role="radio" aria-checked="false" data-value="ur">اردو</button>
</div>
</div>
<div class="demo-row"><span class="label"></span>
<label class="toggle"><input type="checkbox" data-key="international"> International only</label>
<label class="toggle"><input type="checkbox" data-key="lenient"> Lenient</label>
</div>
</div>
<div class="demo-field">
<label class="demo-label" for="phone">Phone</label>
<input id="phone" autocomplete="off">
</div>
<section class="verdict" aria-live="polite" data-state="idle">
<span class="badge" id="badge">waiting</span>
<p id="message">Type a number — try <span class="demo-chip try">050 1</span> <span class="demo-chip try">+44 7400 123456</span> <span class="demo-chip try">٠٣٠٠١٢٣٤٥٦٧</span></p>
</section>
<pre class="code" id="rule">'phone' => ['required', Phone::rule()]</pre>
<dl class="demo-kv">
<div><dt>e164()</dt><dd data-f="e164">—</dd></div>
<div><dt>international()</dt><dd data-f="international">—</dd></div>
<div><dt>national()</dt><dd data-f="national">—</dd></div>
<div><dt>type()</dt><dd data-f="type">—</dd></div>
<div><dt>country()</dt><dd data-f="country">—</dd></div>
<div><dt>masked()</dt><dd data-f="masked">—</dd></div>
</dl>
<p class="demo-hint">Answered by <code>devix-labs/laravel-phone</code> running on devix.pk.</p>
</main>
import { createPhoneInput } from 'https://www.devix.pk/cdn/oss/phone-input@latest/index.js';
const endpoint = 'https://www.devix.pk/api/oss/laravel-phone/playground';
const input = document.getElementById('phone');
const phone = createPhoneInput(input, { initialCountry: 'ae', preferredCountries: ['ae', 'sa', 'pk'], validateOn: 'off' });
const state = { countries: '', types: '', locale: 'en', international: false, lenient: false };
const $ = (id) => document.getElementById(id);
const verdict = document.querySelector('.verdict');
let timer = 0;
let request = 0;
async function check() {
const id = ++request;
const body = new URLSearchParams({
number: input.value,
country: phone.getCountry().code,
countries: state.countries,
types: state.types,
locale: state.locale,
international: state.international ? '1' : '0',
lenient: state.lenient ? '1' : '0',
});
try {
const response = await fetch(endpoint, { method: 'POST', body });
const data = await response.json();
if (id !== request) return;
if (!response.ok) throw new Error(data.error || 'Request failed');
render(data);
} catch (error) {
if (id !== request) return;
verdict.dataset.state = 'error';
$('badge').textContent = 'offline';
$('message').textContent = error.message;
}
}
function render(data) {
const empty = !input.value.trim();
verdict.dataset.state = empty ? 'idle' : data.passes ? 'pass' : 'fail';
$('badge').textContent = empty ? 'waiting' : data.passes ? 'passes' : data.reason;
if (!empty) {
$('message').textContent = data.passes ? 'Valid — this is what your controller receives.' : data.message;
$('message').dir = state.locale === 'en' ? 'ltr' : 'rtl';
}
$('rule').textContent = `'phone' => ['required', ${data.rule}]`;
for (const dd of document.querySelectorAll('[data-f]')) {
const value = data.number[dd.dataset.f];
dd.textContent = value === null || value === '' || (dd.dataset.f === 'type' && value === 'unknown') ? '—' : value;
}
}
const schedule = () => {
clearTimeout(timer);
timer = setTimeout(check, 180);
};
input.addEventListener('input', schedule);
input.addEventListener('dx:countrychange', schedule);
for (const group of document.querySelectorAll('[role="radiogroup"]')) {
group.addEventListener('click', (event) => {
const button = event.target.closest('button[data-value]');
if (!button) return;
for (const other of group.querySelectorAll('button')) other.setAttribute('aria-checked', String(other === button));
state[group.dataset.key] = button.dataset.value;
check();
});
}
for (const box of document.querySelectorAll('input[type="checkbox"][data-key]')) {
box.addEventListener('change', () => {
state[box.dataset.key] = box.checked;
check();
});
}
for (const example of document.querySelectorAll('.try')) {
example.addEventListener('click', async () => {
await phone.setNumber(example.textContent);
input.focus();
check();
});
}
phone.ready.then(check);
@import url('https://www.devix.pk/cdn/oss/phone-input@latest/phone-input.min.css');
.controls { display: grid; gap: 7px; padding: 10px 12px; }
.label { width: 70px; font-size: 12px; color: var(--demo-muted); }
.toggle { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--demo-muted); cursor: pointer; }
.toggle input { accent-color: var(--demo-accent); }
.verdict { display: flex; align-items: flex-start; gap: 8px; min-height: 40px; padding: 9px 11px; border: 1px solid var(--demo-line); border-radius: 8px; background: var(--demo-surface); }
.badge { flex: none; padding: 1px 7px; border-radius: 999px; background: var(--demo-code); color: var(--demo-muted); font: 500 11px var(--demo-mono); }
.verdict p { margin: 1px 0 0; font-size: 13px; }
.verdict[data-state="pass"] { border-color: color-mix(in srgb, var(--demo-ok) 40%, transparent); }
.verdict[data-state="pass"] .badge { background: color-mix(in srgb, var(--demo-ok) 14%, transparent); color: var(--demo-ok); }
.verdict[data-state="fail"] { border-color: color-mix(in srgb, var(--demo-danger) 40%, transparent); }
.verdict[data-state="fail"] .badge { background: color-mix(in srgb, var(--demo-danger) 12%, transparent); color: var(--demo-danger); }
.code { margin: 0; padding: 8px 10px; overflow-x: auto; border: 1px solid var(--demo-line); border-radius: 8px; background: var(--demo-code); color: var(--demo-fg); font: 11.5px/1.6 var(--demo-mono); white-space: pre; }
.demo-kv { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 460px) { .label { width: 100%; } .demo-kv { grid-template-columns: 1fr 1fr; } }