gs-suite
v0.2.22
Published
Simple set utility stack for SPA applications
Downloads
1,041
Readme
GS-Suite (dev)
GS-Suite adalah framework UI/interaksi berbasis vanilla JavaScript untuk kebutuhan CMS/admin dashboard SPA. Fokus utamanya: lifecycle yang deterministik, runtime yang aman, dan plugin/modul yang modular.
Scope
- Target: aplikasi admin/CMS yang hidup lama (8-12 jam) dengan cleanup yang konsisten.
- Bukan: SSR framework, hydration, atau framework universal.
Prinsip Runtime
gs.boot()hanya boleh dipanggil 1x per runtime.gs.configread-only setelah boot (set lewatgs.setupsebelum boot).- Module handler wajib return disposer (boleh noop). Disposer dipanggil saat route change dan destroy app.
- Error code konsisten lewat
gs.event.ERROR_CODE.
Quick Start
import { gs } from 'gs-suite';
gs.setup({
config: {
router: { base: '' },
log: { level: 'debug' }
}
});
gs.boot({
root: '#app',
modules: () => {
gs.module.setLayer('screen_home', '<div wallscreen></div>');
gs.module.setLayer('layout_home', '<div>Home</div>');
gs.module.register({
init: 'home',
path: 'home',
screen: 'screen_home',
layout: 'layout_home',
handler() {
return () => {
// disposer
};
}
});
gs.router.register({
'/home': () => 'home'
});
},
router: { start: true }
});Error Codes
Gunakan gs.event.ERROR_CODE untuk mendeteksi error inti (contoh: GS_BOOT_ALREADY_CALLED, GS_ROOT_NOT_FOUND, GS_MODULE_NO_DISPOSER).
GS-Suite
GS-Suite adalah framework JavaScript opinionated untuk Admin / CMS Single-Page Applications (SPA).
Dirancang untuk: • Dashboard internal • CMS • Backoffice • Data-heavy admin tools
Bukan untuk: • Landing page publik • SEO / SSR • Website marketing • Menggantikan React / Vue / Next.js
⸻
Why GS-Suite?
GS-Suite fokus pada stabilitas jangka panjang admin SPA, bukan tren UI. • Explicit lifecycle — tidak ada auto-run • Deterministic boot — satu pintu gs.boot() • Runtime safety — read-only setelah ready • Mandatory cleanup — anti memory leak • Security-aware DOM — aman untuk admin • Zero framework dependency — ringan & dapat dikontrol
⸻
Core Principles 1. Host-controlled HTML GS-Suite tidak membuat index.html. Host yang menyediakan container. 2. One-time boot Aplikasi hanya boleh di-boot sekali. Tidak ada side-effect tersembunyi. 3. Lifecycle first Setiap module/plugin harus bisa dibersihkan (disposer). 4. Admin-first, opinionated Privilege, guard, dan workflow adalah use-case utama.
minimal usage
import gs from 'gs-suite'
gs.boot({
root: '#app',
debug: true,
adapter: {
layerLoader: async (name) => fetch(/layers/${name}).then(r => r.text()),
authGuard: async () => true
}
})
When NOT to use GS-Suite • Anda butuh SSR atau SEO publik • Anda membangun landing page marketing • Anda ingin ecosystem React/Vue • Anda ingin UI library dengan banyak theme
GS-Suite
Framework Vanilla JS ringan untuk SPA (tanpa dependency). Fokus pada modularisasi, plugin UI, dan flow SPA sederhana.
Instalasi
npm install gs-suiteCLI Scaffold
npx gs-suite create gs-suite-appOpsional:
npx gs-suite create gs-suite-app --template blank
npx gs-suite create gs-suite-app --force --no-installFlag:
--template <name>:basicataublank(default:basic)--force: overwrite jika folder target sudah berisi file--no-install: skipnpm install
Import Dasar
import { gs, store } from 'gs-suite';Struktur runtime:
gsadalah facade utama (event, dom, plugin, module).storeadalah instance state helper (gs.state.create) untuk menyimpan data global.
Contoh penggunaan store:
store.set('user', { id: 1, name: 'Dipa' });
const user = store.get('user');Konfigurasi Global
gs.setup({
config: {
env: 'development',
debug: ['localhost:8092'],
log: {
level: 'auto', // 'auto' | 'debug' | 'info' | 'warn' | 'error'
silent: false,
},
path: {
publish: '',
additional: {},
},
request: {
baseUrl: '',
credentials: 'same-origin',
timeout: 15000,
},
router: {
mode: 'history', // 'history' | 'hash'
linkAttr: 'data-route',
backAttr: 'data-route-back',
scrollRestoration: 'auto', // 'auto' | 'manual' | false
},
html: {
mode: 'allow', // 'allow' | 'warn' | 'block'
sanitizer: null,
},
locale: {
locale: 'id-ID',
timeZone: 'Asia/Jakarta',
},
performance: {
useRAF: true,
batchDOM: true,
},
runtime: {
freeze: 'auto', // 'auto' | true | false
},
feature: {
mode: 'warn', // 'silent' | 'warn' | 'throw'
flags: {
inputpicker: false,
},
},
}
});
Catatan `feature`:
- `feature.flags` atau direct key `feature.<plugin>` bisa dipakai (backward compatible).
- `mode: 'warn'` → log warning, method jadi no-op.
- `mode: 'silent'` → no-op tanpa log.
- `mode: 'throw'` → akan throw (saat bootstrap/setup jika plugin missing, atau saat method dipanggil jika plugin disabled).
Catatan `runtime.freeze` (legacy):
- `gs.boot` akan mengunci runtime setelah ready. Opsi ini disimpan untuk kompatibilitas lama.Catatan html.mode:
allow(default): render HTML seperti biasa.warn: render HTML + tampilkan warning console.block: semua HTML akan diperlakukan sebagai textContent. Gunakangs.event.setHTML(element, html)untuk memasukkan HTML (kecuali editor).
Boot (Direkomendasikan)
await gs.boot({
root: '#app',
router: {
base: '',
start: true,
},
plugins: [
{ name: 'form', plugin: gs.plugin.form },
{ name: 'notification', plugin: gs.plugin.notification },
{ name: 'flybox', plugin: gs.plugin.flybox },
{ name: 'popup', plugin: gs.plugin.popup },
],
modules: async () => {
const registerModules = (await import('./src/registry/modules.js')).default;
registerModules();
}
});Catatan gs.boot:
rootwajib disediakan (selector atau HTMLElement).- Plugin hanya boleh diregister saat booting.
- Runtime akan dikunci (read-only) setelah ready.
gs.setupdangs.bootstrapdipertahankan untuk legacy.
Bootstrap
await gs.bootstrap({
modules: async () => {
const registerModules = (await import('./modules/index.js')).default;
registerModules();
},
plugins: async () => {
const registerPlugins = (await import('./plugins/index.js')).default;
registerPlugins();
},
autoNavigate: true,
});Router SPA (Resmi)
Router hanya memetakan URL ke module. Tidak render DOM secara langsung.
gs.router.register({
'/': () => ({ init: 'home' }),
'/users': () => ({ init: 'user_list' }),
'/users/:id': (params) => ({
init: 'user_detail',
data: { userId: params.id }
}),
});
gs.router.init();Navigasi:
gs.router.push('/users/12?tab=info');Hook:
gs.router.beforeEach((to, from) => {
if (to.path.startsWith('admin') && !store.get('user')) return false;
});
gs.router.afterEach((to, from) => {
console.log('route change', from, '->', to);
});Catatan:
- Elemen dengan atribut
data-route(anchor atau non-anchor) akan dicegat router. - Jika memakai non-anchor, tambahkan
role="link"dantabindex="0"untuk aksesibilitas. - Elemen dengan
data-route-backakan memanggilhistory.back(). gs.module.navigator()masih ada tapi deprecated (gunakan router).
Sistem Module (SPA)
GS-Suite memakai konsep module berbasis URL. Tiap module memiliki screen, layout, setup, handler, closing, dan cleanup.
Contoh Register Module
gs.module.register({
init: 'home',
path: 'home',
auth: false,
screen: 'screen_main',
layout: 'page_home',
setup: async (wall, data) => {
// siapin state atau fetch data
},
handler: (wall) => {
// scan plugin pada scope wall
},
closing: () => {
// animasi close (opsional)
},
cleanup: () => {
// bersihkan popup/flybox/plugin state (opsional)
}
});Setup Module (Layer & Routes)
gs.module.setup({
layers: {
screen_main: '<section wallscreen></section>',
'gs_404': '<section>404</section>',
},
routes: {
index: 'home',
direct: '404',
},
list: [
{ init: 'home', path: 'home', screen: 'screen_main', layout: 'page_home' }
],
});Wajib: layout screen_* harus memiliki elemen [wallscreen] untuk render konten module.
State Helper (Resmi)
State helper bersifat manual (tidak ada auto render).
const appState = gs.state.create({ user: null, loading: false });
const unwatch = appState.watch('user', (next, prev) => {
console.log('user change', prev, '->', next);
});
appState.set('user', { id: 2 });
unwatch();Data route yang masuk ke module akan disimpan ke store:
const moduleData = store.get('module');Plugin Umum
Pola umum:
register()(jika ada)setup()(sekali global)scan(root)saat DOM siap atau setelah render module
Form
Markup:
<form gs-form="user_add">
<input gs-field="name" />
<button type="button" gs-form-submit="user_add">Save</button>
</form>Script:
gs.plugin.form.register({
init: 'user_add',
fields: { name: '' },
submit: (data) => { console.log(data); }
});
gs.plugin.form.setup();
gs.plugin.form.scan('user_add');InputPicker
Markup:
<div gs-inputpicker="privileges">
<input gs-inputpicker-key="privileges" data-value="" />
</div>Script:
gs.plugin.inputPicker.register({
init: 'privileges',
options: [
{ id: 1, value: 'Admin' },
{ id: 2, value: 'Editor' },
],
multi: false,
onChange: ({ id }) => console.log(id),
});
gs.plugin.inputPicker.setup();
gs.plugin.inputPicker.scan(document);SelectPicker
Markup:
<button gs-selectpicker="region" type="button"></button>
<div gs-selectpicker-options="region"></div>Script:
gs.plugin.selectpicker.register({
init: 'region',
options: [
{ id: 'id', value: 'Indonesia' },
{ id: 'sg', value: 'Singapore' },
],
onChange: ({ id }) => console.log(id),
});
gs.plugin.selectpicker.setup();
gs.plugin.selectpicker.scan(document);InputDatepicker
Markup:
<input gs-inputdatepicker-key="date_main" data-value="" />
<div gs-inputdatepicker-options="date_main"></div>Script:
gs.plugin.inputDatepicker.register({
init: 'date_main',
range: false,
allowManualInput: true,
});
gs.plugin.inputDatepicker.setup();
gs.plugin.inputDatepicker.scan(document);Editor (WYSIWYG)
Markup:
<div gs-editor="article_body"></div>
<input type="hidden" name="body" gs-editor-input="article_body" />
<div gs-editor-toolbar="article_body">
<button type="button" gs-editor-cmd="bold">B</button>
<button type="button" gs-editor-cmd="italic">I</button>
<button type="button" gs-editor-cmd="underline">U</button>
<button type="button" gs-editor-link>Link</button>
<button type="button" gs-editor-unlink>Unlink</button>
<button type="button" gs-editor-block="article_body"></button>
<div gs-editor-block-options="article_body"></div>
</div>Script:
gs.plugin.editor.register({
init: 'article_body',
placeholder: 'Tulis konten...',
pasteMode: 'clean',
sanitizeOnInput: true,
});
gs.plugin.editor.setup();
gs.plugin.editor.scan(document);Catatan:
- Paste otomatis dibersihkan sesuai whitelist tag/atribut.
data-routemasih bisa digunakan di luar editor, editor fokus ke konten saja.
Colorpicker
gs.plugin.colorpicker.register({ init: 'color_main' });
gs.plugin.colorpicker.setup();
gs.plugin.colorpicker.scan(document);Tag
Markup:
<div gs-tag="tag_main">
<div gs-tag-display="tag_main"></div>
<input gs-tag-key="tag_main" />
</div>Script:
gs.plugin.tag.register({ init: 'tag_main', delimiter: ',' });
gs.plugin.tag.setup();
gs.plugin.tag.scan(document);Datatable + Paging
gs.plugin.datatable.register({
init: 'user',
form: 'user_form',
tablePlacement: 'placement_table',
paginationPlacement: 'placement_paging',
});
gs.plugin.datatable
.lock('user')
.setTableContent([
{ label: 'No', item: 'no' },
{ label: 'Name', item: 'name', sort: true },
{ label: 'Email', item: 'email' },
])
.open();
gs.plugin.datatable.setData('user', {
paging: { page: 1, last: 3, total: 100, limit: 10, next: 2, prev: 0, first: 1, from: 0 },
response: [{ name: 'A', email: '[email protected]' }],
});
gs.plugin.datatable.lock('user').scan();Paging (standalone)
const pagingDom = gs.plugin.paging.generate('user_form', pagingData);
document.querySelector('#paging').append(pagingDom);
gs.plugin.paging.setup();Motion (IntersectionObserver)
gs.plugin.motion.register({
init: 'main-screen',
motionCalls: [{ stack: 'mt-bottom', staging: 'mt-on-bottom' }],
showMode: 'all',
autorun: true,
});
gs.plugin.motion.setup();
gs.plugin.motion.scan('main-screen');Popup
gs.plugin.popup.register({
init: 'detail',
handler: ({ wall }) => { gs.event.setHTML(wall, '<div>Popup</div>'); }
});
gs.plugin.popup.setup();
gs.plugin.popup.open({ init: 'detail' });Flybox (Overlay)
const entry = gs.plugin.flybox.open({
trigger: document.querySelector('#btn'),
handler: ({ wall }) => { gs.event.setHTML(wall, '<div>Flybox</div>'); },
});
// close:
gs.plugin.flybox.close(entry);Notification
gs.plugin.notification.push({ message: 'Saved', type: 'success' });Autogrow (Textarea)
gs.plugin.autogrow.setup({ element: '[auto-grow]' });
gs.plugin.autogrow.scan(document);Input Radio / Checkbox
gs.plugin.inputRadio.setup();
gs.plugin.inputRadio.scan(document);
gs.plugin.inputCheckbox.setup();
gs.plugin.inputCheckbox.scan(document);Uploader Image
Markup:
<div gs-uploader="avatar">
<input gs-uploader-value />
<div gs-uploader-display></div>
<button type="button" gs-uploader-open>Upload</button>
</div>Script:
gs.plugin.uploaderImage.register({ init: 'avatar', path: 'uploader' });
gs.plugin.uploaderImage.setup();
gs.plugin.uploaderImage.scan('avatar');Input Price / Phone
gs.plugin.inputPrice.setup();
gs.plugin.inputPrice.scan(document);
gs.plugin.inputPhone.setup();
gs.plugin.inputPhone.scan(document);Referensi API
- Detail API tersedia di
gs-documentation.json.
Contoh Struktur Project
src/
gs/
plugins/
index.js
popup.js
inputPicker.js
datatable.js
modules/
index.js
module.home.js
module.userAdd.js
pages/
page_home.html
page_user_add.html
screens/
screen_main.html
screen_auth.htmlContoh src/gs/modules/index.js:
import home from './module.home.js';
import userAdd from './module.userAdd.js';
export default () => {
home();
userAdd();
};Contoh src/gs/plugins/index.js:
import popup from './popup.js';
import inputPicker from './inputPicker.js';
import datatable from './datatable.js';
export default () => {
popup();
inputPicker();
datatable();
};Best Practices
- Selalu panggil
scan(root)dengan scope module (hindari scan seluruhdocument) untuk performa. - Gunakan
cleanuppadags.module.registeruntuk menutup popup/flybox dan reset state plugin saat pindah module. - Atur
html.modesesuai kebutuhan keamanan:blockuntuk memastikan tidak ada HTML injection.warnuntuk debug input HTML tanpa memblok.allowuntuk kebutuhan HTML penuh (pastikan sanitasi data sendiri).
