Skip to content
/ client Public

The simplest, fastest and most organized way to manage the front-end of web applications.

License

Notifications You must be signed in to change notification settings

je-es/client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


logo


CI Test Coverage Github Repo Issues GitHub Repo stars

  • Quick Start 🔥

    The simplest, fastest and most organized way to manage the front-end of web applications.

    This lib must run with @je-es/server.

    We prefer to use space with @solution-dist/web for a better experience.

    • Setup

      install space first.

      • Create

        > space init <name> -t web    # This will clone a ready-to-use repo and make some changes to suit your app.
        > cd <name>                   # Go to the project directory
        > space install               # Install the dependencies
      • Manage

        > space lint
        > space build                 # Builds both JavaScript and SCSS
        > space test
        > space start
        > space watch                 # Development mode with auto-rebuild
      • Usage

        import { Component, html, state, router } from '@je-es/client';
        
        class MyComponent extends Component {
            @state count = 0;
        
            render() {
                return html`
                    <div>
                        <h1>Count: ${this.count}</h1>
                        <button onclick=${() => this.count++}>
                            Increment
                        </button>
                    </div>
                `;
            }
        
            styles() {
                return css`
                    .my-component {
                        padding: 2rem;
                        background: var(--bg-color-primary);
                    }
                `;
            }
        }
         > space start
        
           - Building @je-es/client application...
           - Build completed successfully!
        
           Server started at http://localhost:3000
    line
  • Examples

    • Component System

      import { Component, html, css, state, computed } from '@je-es/client';
      
      class TodoComponent extends Component {
          @state todos = [];
          @state filter = 'all';
      
          @computed
          get filteredTodos() {
              return this.filter === 'all'
                  ? this.todos
                  : this.todos.filter(t => t.status === this.filter);
          }
      
          render() {
              return html`
                  <div class="todo-app">
                      <h1>My Todos</h1>
                      ${this.filteredTodos.map(todo => html`
                          <div class="todo-item">
                              ${todo.title}
                          </div>
                      `)}
                  </div>
              `;
          }
      
          styles() {
              return css`
                  .todo-app {
                      padding: 2rem;
                      max-width: 600px;
                      margin: 0 auto;
                  }
                  .todo-item {
                      padding: 1rem;
                      border-bottom: 1px solid #eee;
                  }
              `;
          }
      }

      Components that manage their own DOM directly (like Notifications and ItemsLoader) shouldn't use @state decorators for properties that trigger re-renders. The @state decorator is designed for components that rely on virtual DOM patching. When you manage DOM directly with appendChild/remove, re-renders destroy your manual DOM changes.

      line
    • SCSS Styling System

      import { client } from '@je-es/client';
      
      // Configure build with SCSS support
      const app = client({
          build: {
              entry: './app/main.ts',
              output: './static/client.js',
              minify: true,
              sourcemap: true,
              optimization: {
                  splitChunks: false,
                  treeShaking: true,
              },
              // SCSS Configuration
              styles: {
                  input: './app/style',               // SCSS files directory
                  output: './static/css/client.css',  // Compiled CSS output
              },
          },
          app: {
              root: '#app',
          },
      });
      line
    • Router Configuration

      import { client, router } from '@je-es/client';
      
      const clientApp = client({
          app: {
              root: '#app',
              routes: [
                  {
                      path: '/',
                      component: () => import('./pages/HomePage'),
                      meta: { title: 'Home' }
                  },
                  {
                      path: '/users/:id',
                      component: () => import('./pages/UserPage'),
                      meta: { title: 'User Profile' },
                      beforeEnter: (to, from, next) => {
                          // Route guard
                          if (isAuthenticated()) {
                              next();
                          } else {
                              next('/login');
                          }
                      }
                  }
              ]
          },
          router: {
              mode: 'history',
              base: '/',
              scrollBehavior: 'smooth'
          }
      });
      
      // Navigate programmatically
      router.push('/users/123');
      router.replace('/home');
      router.back();
      line
    • State Management (Store)

      import { createStore, connect } from '@je-es/client';
      
      // Create global store
      const userStore = createStore({
          state: {
              user: null,
              isAuthenticated: false,
              preferences: {}
          },
          persist: true,
          storage: 'localStorage',
          storageKey: 'app-user-store'
      });
      
      // Subscribe to changes
      userStore.subscribe((state) => {
          console.log('User state changed:', state);
      });
      
      // Update state
      userStore.setState({
          user: { name: 'John', email: 'john@example.com' },
          isAuthenticated: true
      });
      
      // Connect to component
      class UserProfile extends Component {
          render() {
              return html`<div>User: ${userStore.state.user?.name}</div>`;
          }
      }
      
      connect(userStore, component, (state) => ({
          user: state.user
      }));
      line
    • React-Like Hooks

      import {
          createFunctionalComponent,
          useState,
          useEffect,
          useMemo
      } from '@je-es/client';
      
      const Counter = createFunctionalComponent((props) => {
          const [count, setCount] = useState(0);
          const [multiplier, setMultiplier] = useState(2);
      
          // Computed value
          const result = useMemo(() => {
              return count * multiplier;
          }, [count, multiplier]);
      
          // Side effect
          useEffect(() => {
              document.title = `Count: ${count}`;
      
              return () => {
                  // Cleanup
                  document.title = 'App';
              };
          }, [count]);
      
          return html`
              <div>
                  <h2>Result: ${result}</h2>
                  <button onclick=${() => setCount(count + 1)}>
                      Increment
                  </button>
                  <button onclick=${() => setMultiplier(multiplier + 1)}>
                      Increase Multiplier
                  </button>
              </div>
          `;
      }, 'Counter');
      
      const component = new Counter({ initial: 5 });
      await component.mount(container);
      line
    • Smart Forms

      import { SmartFormComponent } from '@je-es/client';
      
      const loginForm = new SmartFormComponent({
          fields: [
              {
                  name: 'email',
                  type: 'email',
                  label: 'Email Address',
                  placeholder: 'Enter your email',
                  validation: {
                      required: true,
                      email: true,
                      message: 'Please enter a valid email'
                  }
              },
              {
                  name: 'password',
                  type: 'password',
                  label: 'Password',
                  validation: {
                      required: true,
                      minLength: 8,
                      message: 'Password must be at least 8 characters'
                  }
              },
              {
                  name: 'remember',
                  type: 'checkbox',
                  label: 'Remember me'
              }
          ],
          endpoint: '/api/auth/login',
          method: 'POST',
          autoValidate: true,
          submitButton: {
              label: 'Sign In',
              loadingLabel: 'Signing in...',
              className: 'btn-primary'
          },
          onSuccess: (data) => {
              localStorage.setItem('token', data.token);
              router.push('/dashboard');
          },
          onError: (error) => {
              console.error('Login failed:', error);
          }
      });
      
      await loginForm.mount(container);
      line
    • Context API

      import { createContext, Provider, useContext } from '@je-es/client';
      
      // Create contexts
      const ThemeContext = createContext({ theme: 'light' });
      const UserContext = createContext({ user: null });
      
      // Provider component
      class App extends Component {
          @state theme = 'dark';
          @state user = { name: 'John' };
      
          render() {
              return html`
                  <${Provider}
                      context=${ThemeContext}
                      value=${{ theme: this.theme }}
                  >
                      <${Provider}
                          context=${UserContext}
                          value=${{ user: this.user }}
                      >
                          <${ConsumerComponent} />
                      </${Provider}>
                  </${Provider}>
              `;
          }
      }
      
      // Consumer component
      class ConsumerComponent extends Component {
          render() {
              const theme = useContext(ThemeContext, this);
              const user = useContext(UserContext, this);
      
              return html`
                  <div class="${theme.theme}">
                      Welcome, ${user.user?.name}!
                  </div>
              `;
          }
      }
      line
    • Internationalization (i18n)

      Setup in App Initialization:

      import { setupI18n, t } from '@je-es/client';
      
      // In your app initialization (browser.ts or similar)
      async function initializeApp() {
          // 1. Setup i18n - loads the currently selected language
          //    (from localStorage if user visited before, otherwise default)
          await setupI18n({
              defaultLanguage: 'en',
              supportedLanguages: ['en', 'ar'],
              staticPath: 'static/i18n'
          });
      
          // 2. Now safe to use t() and render components
          const app = new MyApp();
          await app.init();
      }
      
      // Call when DOM is ready
      if (document.readyState === 'loading') {
          document.addEventListener('DOMContentLoaded', initializeApp);
      } else {
          initializeApp();
      }

      Use t() Anywhere:

      import { t, setLanguage, loadLanguageFile, getCurrentLanguage } from '@je-es/client';
      
      // After setupI18n() resolves, you can use t() directly everywhere!
      console.log(t('hello')); // Works in current language!
      console.log(t('welcome', { app_name: 'MyApp' }));
      
      // Switch language with lazy-loading (loads file only when needed)
      async function selectLanguage(lang: string) {
          await loadLanguageFile(lang);  // Load language on-demand
          setLanguage(lang);
          console.log(getCurrentLanguage()); // New language
      }

      Lazy-Loading Languages for Performance:

      setupI18n() loads the currently selected language on page load:

      • First visit: Loads default language (e.g., en.json)
      • After switching: Loads new language on-demand
      • Returning user: Loads their previously selected language from localStorage
      // Returning user who previously selected Arabic
      // Page loads with ar.json (their choice) - fast! ⚡
      await setupI18n({
          defaultLanguage: 'en',
          supportedLanguages: ['en', 'ar', 'fr'],
          staticPath: 'static/i18n'
      });
      
      // User clicks to switch language
      await loadLanguageFile('fr');  // Load only when needed
      setLanguage('fr');

      In Components:

      import { Component, html } from '@je-es/client';
      import { t, setLanguage, loadLanguageFile, createTranslator } from '@je-es/client';
      
      class MultiLanguageComponent extends Component {
          render() {
              return html`
                  <div>
                      <h1>${t('welcome', { app_name: 'JE-ES' })}</h1>
                      <button onclick=${() => this.switchLang('en')}>
                          English
                      </button>
                      <button onclick=${() => this.switchLang('ar')}>
                          العربية
                      </button>
                  </div>
              `;
          }
      
          async switchLang(lang: string) {
              await loadLanguageFile(lang);
              setLanguage(lang);
              this.refresh();
          }
      
          connectedCallback() {
              super.connectedCallback();
              // Subscribe to language changes
              this.unsubscribe = createTranslator(() => this.refresh());
          }
      
          disconnectedCallback() {
              super.disconnectedCallback();
              if (this.unsubscribe) {
                  this.unsubscribe();
              }
          }
      }

      Advanced Usage:

      import { t, tLang, loadLanguage, getSupportedLanguages, hasKey } from '@je-es/client';
      
      // Translate with specific language temporarily
      console.log(t('hello')); // Current language
      console.log(tLang('hello', 'en')); // Temporarily use English
      
      // Load specific language dynamically
      loadLanguage('fr', {
          hello: 'Bonjour',
          welcome: 'Bienvenue dans {app_name}'
      });
      
      // Check supported languages
      console.log(getSupportedLanguages()); // ['en', 'ar', 'fr']
      
      // Check if key exists
      if (hasKey('custom_key')) {
          console.log(t('custom_key'));
      }

      Features:

      • Lazy-loaded languages: Default language loads on init, others load on-demand
      • Fast page load: Only default language file is downloaded initially
      • localStorage support: Automatically saves language preference
      • Parameter replacement: Replace placeholders with dynamic values
      • Nested translations: Use translation keys as parameter values
      • Reactive updates: Listen to language changes with createTranslator()
      • Fallback language: Falls back to configured default language if translation is missing
      • Multiple languages: Support for unlimited languages
      • URL loading: Load translations from remote JSON files or manually with loadLanguageFile()
      line
    • API Integration

      import { api, http, configureApi } from '@je-es/client';
      
      // Global configuration
      configureApi({
          baseURL: 'https://api.example.com',
          timeout: 30000,
          headers: {
              'Content-Type': 'application/json'
          },
          interceptors: {
              request: (config) => {
                  const token = localStorage.getItem('token');
                  if (token) {
                      config.headers['Authorization'] = `Bearer ${token}`;
                  }
                  return config;
              },
              response: (response) => {
                  console.log('Response:', response);
                  return response;
              },
              error: (error) => {
                  if (error.status === 401) {
                      router.push('/login');
                  }
                  throw error;
              }
          }
      });
      
      // Make requests
      const users = await http.get('/users');
      const user = await http.get('/users/123');
      const created = await http.post('/users', { name: 'Jane' });
      const updated = await http.put('/users/123', { name: 'Jane Doe' });
      await http.delete('/users/123');
      
      // Advanced usage
      const response = await api({
          method: 'POST',
          url: '/upload',
          data: formData,
          params: { folder: 'avatars' },
          timeout: 60000
      });

  • Build System

    • Build Configuration

      import { client } from '@je-es/client';
      
      const app = client({
          build: {
              // JavaScript Configuration
              entry: './app/main.ts',
              output: './static/client.js',
              minify: true,                    // Minify JS in production
              sourcemap: true,                 // Generate source maps
              optimization: {
                  splitChunks: false,          // Code splitting
                  treeShaking: true,           // Remove unused code
              },
      
              // SCSS Configuration
              styles: {
                  input: './app/style',          // SCSS directory
                  output: './static/client.css', // CSS output
              },
          },
          app: {
              root: '#app',
              mode: 'spa',
          },
          router: {
              mode: 'history',
              base: '/',
          },
          devTools: {
              enabled: true,
          },
      });
      
      // Build script
      if (import.meta.main) {
          await app.build();
      }
      
      // Runtime (in browser)
      if (typeof window !== 'undefined') {
          app.init();
      }

      Note: i18n is now configured directly in your app initialization via setupI18n() instead of in the client config. See the Internationalization section for details.

      line
    • Build Commands

      # Production build (minified)
      bun run build
      
      # Development build (with source maps)
      bun run build --dev
      
      # Watch mode (auto-rebuild on changes)
      bun run watch
      
      # Clean build directory
      bun run clean

      Build Output:

      - Building @je-es/client application...
      - Build completed successfully!
      
      line
    • Watch Mode for Development

      // watch.ts or dev script
      import { client } from '@je-es/client';
      
      const app = client({
          build: {
              entry: './app/main.ts',
              output: './static/client.js',
              minify: false,
              sourcemap: true,
              styles: {
                  input: './app/style',
                  output: './static/client.css',
              },
          },
      });
      
      // Start watch mode
      await app.watch();
      # Terminal output:
      đź‘€ Watching for changes...
      🔄 main.ts changed, rebuilding JS...
      âś… JS rebuild complete
      🔄 layout.scss changed, rebuilding CSS...
      âś… CSS rebuild complete

  • API

    • Component Lifecycle

      class MyComponent extends Component {
          // Called before component is mounted to DOM
          async onBeforeMount(): void {
              // Initialize data, fetch resources
          }
      
          // Called after component is mounted to DOM
          async onMount(): void {
              // Setup event listeners, start timers
          }
      
          // Called before component updates
          async onBeforeUpdate(prevProps, prevState): void {
              // Prepare for update
          }
      
          // Called after component updates
          onUpdate(prevProps, prevState): void {
              // React to changes
          }
      
          // Called before component unmounts
          onBeforeUnmount(): void {
              // Cleanup preparation
          }
      
          // Called after component unmounts
          onUnmount(): void {
              // Remove listeners, clear timers
          }
      
          // Called when error occurs
          onError(error: Error, errorInfo): void {
              // Handle errors
          }
      
          // Control whether component should update
          shouldUpdate(prevProps, prevState): boolean {
              return true; // or custom logic
          }
      }
      line
    • Decorators

      import { state, computed, watch } from '@je-es/client';
      
      class ReactiveComponent extends Component {
          // Reactive state - triggers re-render on change
          @state count = 0;
          @state items = [];
          @state user = { name: 'John' };
      
          // Computed property - cached until dependencies change
          @computed
          get doubleCount() {
              return this.count * 2;
          }
      
          @computed
          get itemCount() {
              return this.items.length;
          }
      
          // Watch for property changes
          @watch('count')
          onCountChange(newValue, oldValue) {
              console.log(`Count changed from ${oldValue} to ${newValue}`);
          }
      
          @watch('user')
          onUserChange(newUser, oldUser) {
              console.log('User updated:', newUser);
          }
      
          render() {
              return html`
                  <div>
                      Count: ${this.count}
                      Double: ${this.doubleCount}
                  </div>
              `;
          }
      }
      line
    • React-Like Hooks

      import {
          useState,
          useEffect,
          useMemo,
          useCallback,
          useRef,
          useReducer,
          useLocalStorage,
          useDebounce,
          usePrevious,
          useToggle,
          useInterval,
          useFetch,
          useWindowSize,
          useEventListener
      } from '@je-es/client';
      
      // State management
      const [count, setCount] = useState(0);
      const [user, setUser] = useState({ name: 'John' });
      
      // Side effects
      useEffect(() => {
          console.log('Component mounted');
          return () => console.log('Component unmounted');
      }, []);
      
      // Memoization
      const expensiveValue = useMemo(() => {
          return count * 2;
      }, [count]);
      
      // Callback memoization
      const handleClick = useCallback(() => {
          setCount(count + 1);
      }, [count]);
      
      // Persistent reference
      const inputRef = useRef(null);
      
      // Complex state
      const [state, dispatch] = useReducer(reducer, initialState);
      
      // LocalStorage sync
      const [value, setValue] = useLocalStorage('key', defaultValue);
      
      // Debounced value
      const debouncedSearch = useDebounce(searchTerm, 500);
      
      // Previous value
      const prevCount = usePrevious(count);
      
      // Boolean toggle
      const [isOn, toggle] = useToggle(false);
      
      // Interval
      useInterval(() => {
          console.log('Tick');
      }, 1000);
      
      // Data fetching
      const { data, loading, error, refetch } = useFetch('/api/users');
      
      // Window size
      const { width, height } = useWindowSize();
      
      // Event listener
      useEventListener('click', handleClick, element);
      line
    • Router API

      import { router, Router } from '@je-es/client';
      
      // Navigation
      router.push('/users/123');
      router.push('/search?q=test&page=2');
      router.replace('/home');
      router.back();
      router.forward();
      router.go(-2);
      
      // Named routes
      router.pushNamed('user', { id: '123' });
      
      // Route guards
      router.beforeEach((to, from, next) => {
          if (to.meta.requiresAuth && !isAuthenticated()) {
              next('/login');
          } else {
              next();
          }
      });
      
      router.afterEach((to, from) => {
          console.log(`Navigated from ${from.path} to ${to.path}`);
      });
      
      // Route info
      const current = router.getCurrentRoute();
      const isActive = router.isActive('/users');
      const route = router.resolve('/users/123');
      
      // Route outlet (in component)
      render() {
          return html`
              <div>
                  <nav>...</nav>
                  ${router.outlet()}
              </div>
          `;
      }
      line
    • Store API

      import { Store, createStore, createComputedStore, connect } from '@je-es/client';
      
      // Create store
      const store = createStore({
          state: { count: 0, user: null },
          persist: true,
          storage: 'localStorage',
          storageKey: 'my-store',
          middleware: [
              (state, action) => {
                  console.log('State changed:', action, state);
              }
          ]
      });
      
      // Get state
      const state = store.state;
      const count = store.get('count');
      
      // Update state
      store.setState({ count: 5 });
      store.set('count', 10);
      store.setState(prev => ({ count: prev.count + 1 }));
      
      // Subscribe
      const unsubscribe = store.subscribe((state) => {
          console.log('State:', state);
      });
      
      store.subscribeToKey('count', (value) => {
          console.log('Count:', value);
      });
      
      // Batch updates
      store.batch(() => {
          store.set('count', 1);
          store.set('user', { name: 'Jane' });
      });
      
      // Computed store
      const doubleStore = createComputedStore(
          [store],
          (state) => state.count * 2
      );
      
      // Clear/Reset
      store.clear();
      store.reset({ count: 0, user: null });
      
      // Destroy
      store.destroy();
      line
    • Utility Functions

      import {
          debounce,
          throttle,
          classNames,
          formatDate,
          deepClone,
          deepMerge,
          uniqueId,
          sleep,
          isEmpty,
          capitalize,
          kebabCase,
          camelCase,
          pascalCase,
          truncate,
          parseQuery,
          stringifyQuery,
          clamp
      } from '@je-es/client';
      
      // Function utilities
      const debouncedFn = debounce(() => console.log('Called'), 300);
      const throttledFn = throttle(() => console.log('Called'), 1000);
      
      // String utilities
      const classes = classNames('btn', { active: true, disabled: false });
      const date = formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss');
      const cap = capitalize('hello'); // 'Hello'
      const kebab = kebabCase('helloWorld'); // 'hello-world'
      const camel = camelCase('hello-world'); // 'helloWorld'
      const pascal = pascalCase('hello-world'); // 'HelloWorld'
      const short = truncate('Long text here', 10); // 'Long te...'
      
      // Object utilities
      const cloned = deepClone(obj);
      const merged = deepMerge(obj1, obj2, obj3);
      
      // Other utilities
      const id = uniqueId('prefix');
      await sleep(1000);
      const empty = isEmpty(value);
      const clamped = clamp(150, 0, 100); // 100
      
      // Query string
      const params = parseQuery('?page=1&limit=10');
      const query = stringifyQuery({ page: 1, limit: 10 });


About

The simplest, fastest and most organized way to manage the front-end of web applications.

Topics

Resources

License

Stars

Watchers

Forks