Bagaimana saya membangun platform manajemen formulir menggunakan Angular 17 dan SurveyJS

 – Beragampengetahuan
16 mins read

Bagaimana saya membangun platform manajemen formulir menggunakan Angular 17 dan SurveyJS – Beragampengetahuan


Platform survei SaaS seperti SurveyMonkey, Typeform, dan Google Forms memudahkan banyak pengguna untuk membuat survei. Namun, alat ini juga memiliki keterbatasan yang berdampak pada fleksibilitas dan kontrol data.

Permasalahan umum mencakup keterbatasan jumlah respons, keterbatasan opsi penyesuaian antarmuka, keterbatasan opsi penataan formulir, ketergantungan pada layanan pihak ketiga untuk penyimpanan informasi sensitif, dan meningkatnya biaya berlangganan, yang mungkin sulit dipertahankan oleh organisasi yang sedang berkembang.

Untuk organisasi yang ingin mengotomatiskan proses pengumpulan datanya namun masih memerlukan kontrol penuh atas alur kerja datanya, solusi manajemen formulir kustom menawarkan keuntungan yang jelas.

Sistem yang dihosting sendiri memastikan kepemilikan penuh atas data dan infrastruktur, penyesuaian tanpa batas untuk berbagai kasus penggunaan, biaya pengoperasian yang dapat diprediksi (tidak ada batasan berdasarkan penggunaan), dan integrasi langsung dengan sistem lokal. Ketika digunakan bersama-sama, Angular 17 dan SurveyJS menyediakan tumpukan teknologi yang solid untuk membangun platform manajemen formulir khusus yang melebihi kemampuan solusi SaaS komersial mana pun.

Arsitektur yang dijelaskan di sini didasarkan pada tiga komponen utama:

  1. Front-end Angular modern menggunakan komponen independen.
  2. SurveyJS (GitHub) Pembuat visual untuk membuat formulir dan survei.
  3. Backend PHP sederhana yang bertanggung jawab untuk manipulasi data. Apakah Anda bermaksud mengubah sesuatu di artikel?

Kombinasi ini memungkinkan Anda membangun platform yang dapat diskalakan dan dipelihara yang menempatkan semua aspek manajemen formulir sepenuhnya di bawah kendali Anda.

Contents

Pengaturan lingkungan: arsitektur teknis dan ketergantungan

Struktur dan ketergantungan proyek

Tujuan artikel ini bukan untuk memberikan kode dalam jumlah besar, tetapi untuk menjelaskan cara mengatur sistem jenis ini. Kode sumber disediakan terpisah sehingga Anda dapat melihatnya secara detail sambil mengikuti penjelasan strukturalnya.

Pertama, kami mengonfigurasi proyek Angular 17 dan menginstal dependensi SurveyJS yang diperlukan. Arsitektur aplikasi mengikuti struktur modular yang memisahkan tahapan utama alur kerja manajemen formulir:

  • Konstruksi formulir dan survei
  • Render dan visualisasi formulir
  • Pengolahan dan analisis hasil survei

Buat proyek sudut

ng new survey1 --standalone --routing --style=scss
cd survey1

Angular 17 memperkenalkan peningkatan signifikan dalam manajemen komponen independen, menghilangkan kebutuhan akan NgModules dan menyederhanakan arsitektur aplikasi secara keseluruhan. Hal ini menghasilkan struktur yang lebih bersih dan mudah dirawat.

Instal paket SurveyJS

Instal paket SurveyJS utama:

npm install survey-creator-angular survey-angular-ui
npm install @types/survey-creator --save-dev

Konfigurasikan backend PHP

Buat sebuah api Direktori dan atur PHP API:

mkdir api
cd api

Untuk pengembangan lokal, gunakan server PHP bawaan:

php -S localhost:8080

Buat file PHP yang diperlukan untuk titik akhir API Anda. File-file ini akan menangani pembuatan, penyimpanan, dan pengumpulan hasil survei.

Struktur proyek lengkap

Untuk mengilustrasikan penerapannya dengan jelas, bagian berikut menunjukkan bagaimana proyek ini diatur.

Direktori proyek terlihat seperti ini.

Anda dapat melihat kode sumber untuk memahami setiap bagian secara lebih detail. Ide utama kami bisa jadi seperti ini:

Dengan cara ini kita dapat menjelajahi berbagai lapisan aplikasi.

Selidiki antarmuka visual dan interaktivitas komponen Pencipta

Implementasi Pembuat Survei

Pembuat survei adalah komponen inti platform. Ini memungkinkan pengguna untuk merancang survei melalui antarmuka visual. Angular 17 memberikan landasan struktural yang diperlukan untuk mengintegrasikan komponen ini dengan cara yang stabil dan dapat diprediksi.

Antarmuka pembuat formulir mendukung operasi seret dan lepas, konfigurasi logika bersyarat, definisi aturan validasi, dan penyesuaian pengaturan visual untuk survei bangunan. SurveyJS Creator secara otomatis menghasilkan skema JSON yang mewakili struktur setiap formulir.

Skema JSON ini menjadi bagian dari alur kerja manajemen formulir sistem karena disimpan di backend dan kemudian digunakan untuk tugas otomatisasi formulir seperti rendering formulir, pembaruan, serta visualisasi dan analisis data survei.

Seperti inilah tampilan layar utama Creator.

Konfigurasi komponen sudut

Dengan Angular 17 dan komponen independennya, konfigurasi disederhanakan secara signifikan:

// src/app/survey-creator/survey-creator.component.ts
import  Component, OnInit  from '@angular/core';
import  CommonModule  from '@angular/common';
import  SurveyCreatorModel  from 'survey-creator-core';
import  SurveyCreatorModule  from 'survey-creator-angular';
import  SurveyService  from '../survey.service';

@Component(
  selector: 'app-survey-creator',
  standalone: true,
  imports: [CommonModule, SurveyCreatorModule],
  template: `
    <div class="creator-container">
      <h2>Survey Creator </h2>
      <p class="instructions">
        Use the visual editor below to design your survey. 
        Drag questions from the left panel, configure them, and click "Save Survey" when done.
      </p>
     
      
      <div class="creator-wrapper">
        <survey-creator [model]="creator"></survey-creator>
      </div>

      <div class="actions">
        <button (click)="saveSurvey()" [disabled]="saving" class="save-btn">
           saving ? 'Saving... ' : 'Save Survey ' 
        </button>
        <button (click)="loadSurvey()" [disabled]="loading" class="load-btn">
           loading ? 'Loading...' : 'Load Survey ' 
        </button>
      </div>

      <div *ngIf="message" [class]="'message ' + messageType">
         message 
      </div>
    </div>
  `,
  styles: [`
    .creator-container 
      padding: 20px;
    

    h2 
      color: #1976d2;
      margin-bottom: 10px;
    

    .instructions 
      color: #666;
      margin-bottom: 15px;
      line-height: 1.6;
    

    .creator-wrapper 
      margin: 20px 0;
      border: 1px solid #ddd;
      border-radius: 4px;
      overflow: hidden;
    

    .actions 
      display: flex;
      gap: 10px;
      margin-top: 20px;
    

    button 
      padding: 12px 24px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 16px;
      transition: background-color 0.3s;
    

    button:disabled 
      opacity: 0.6;
      cursor: not-allowed;
    

    .save-btn 
      background-color: #4caf50;
      color: white;
    

    .save-btn:hover:not(:disabled) 
      background-color: #45a049;
    

    .load-btn 
      background-color: #2196f3;
      color: white;
    

    .load-btn:hover:not(:disabled) 
      background-color: #1976d2;
    

    .message 
      margin-top: 15px;
      padding: 12px;
      border-radius: 4px;
    

    .message.success 
      background-color: #d4edda;
      color: #155724;
      border: 1px solid #c3e6cb;
    

    .message.error 
      background-color: #f8d7da;
      color: #721c24;
      border: 1px solid #f5c6cb;
    
  `]
)
export class SurveyCreatorComponent implements OnInit {
  creator!: SurveyCreatorModel;
  saving = false;
  loading = false;
  message="";
  messageType: 'success' | 'error' = 'success';

  constructor(private surveyService: SurveyService) 

  ngOnInit() 
    // Initialize Survey Creator
    // Inicializar el Creador de Encuestas
    this.creator = new SurveyCreatorModel(
      showLogicTab: true,
      showJSONEditorTab: true,
      showTestSurveyTab: true,
      showDesignerTab: true
    );

    // Optional: Set default JSON
    // Opcional: Establecer JSON por defecto
    this.creator.JSON = 
      pages: [
        
          name: "page1",
          elements: [
            
              type: "text",
              name: "question1",
              title: "What is your name?"
            
          ]
        
      ]
    ;
  

  /**
   * Save survey to backend
   * Guardar encuesta en el backend
   */
  saveSurvey() 
    this.saving = true;
    this.message="";

    const surveyJson = this.creator.JSON;

    this.surveyService.saveSurvey(surveyJson).subscribe(
      next: (response) => 
        this.saving = false;
        if (response.success) 
          this.showMessage('Survey saved successfully!', 'success');
         else 
          this.showMessage('Error saving survey', 'error');
        
      ,
      error: (error) =>  'Failed to save'), 'error');
      
    );
  

  /**
   * Load survey from backend
   * Cargar encuesta desde el backend
   */
  loadSurvey() 
    this.loading = true;
    this.message="";

    this.surveyService.loadSurvey().subscribe(
      next: (survey) => 
        this.loading = false;
        if (survey && Object.keys(survey).length > 0) 
          this.creator.JSON = survey;
          this.showMessage('Survey loaded successfully! ', 'success');
         else 
          this.showMessage('No survey found ', 'error');
        
      ,
      error: (error) => 
    );
  

  private showMessage(text: string, type: 'success' | 'error') 
    this.message = text;
    this.messageType = type;
    if (type === 'success') 
      setTimeout(() => 
        this.message="";
      , 3000);
    
  
}

Skema JSON: Struktur dan Logika Bersyarat

Dasar-dasar arsitektur SurveyJS

Skema JSON yang dihasilkan oleh SurveyJS Creator mewakili keseluruhan struktur, konten, dan perilaku survei. Ini berisi semua informasi tentang pertanyaan, halaman, dan elemen survei, serta logika kondisional apa pun yang menentukan cara menampilkan atau menyembunyikan pertanyaan berdasarkan respons pengguna. Dengan menggunakan mode ini, Anda dapat membuat survei sederhana dan kompleks tanpa menulis kode tambahan.

Bagian berikut memberikan contoh skema JSON yang dihasilkan oleh SurveyJS Creator:


  "title": "Customer Satisfaction Survey",
  "description": "Help us improve our services",
  "pages": [
    
      "name": "page1",
      "elements": [
        
          "type": "rating",
          "name": "question1",
          "title": "How satisfied are you with our service?",
          "autoGenerate": false,
          "rateCount": 2,
          "rateValues": [
             "value": 1, "text": "Not at all satisfied" ,
             "value": 2, "text": "Very satisfied" 
          ],
          "rateMax": 2
        
      ]
    
  ]

Kita juga dapat melihatnya secara visual seperti ini:

Kita dapat melihat kode JSON menggunakan opsi ini.

Rendering dan UX

Implementasi Penampil Survei

Penampil survei adalah komponen yang menampilkan survei kepada pengguna. Hal ini memastikan bahwa survei ditampilkan secara responsif, beradaptasi dengan berbagai perangkat dan ukuran layar. Kombinasi Angular 17 dan SurveyJS memungkinkan penampil survei bekerja secara efisien dan memungkinkan Anda memberikan pengalaman yang konsisten dan ramah pengguna di desktop, tablet, dan perangkat seluler.

Bagian berikut menunjukkan contoh penampil survei yang menampilkan survei yang dimuat:

Penampil memungkinkan kita melihat survei yang dihasilkan.

Ini adalah kode komponen penampil kami:

import  Component, OnInit  from '@angular/core';
import  CommonModule  from '@angular/common';
import  SurveyModel  from 'survey-core';
import  SurveyModule  from 'survey-angular-ui';
import  SurveyService, SurveySchema  from '../survey.service';

@Component(
  selector: 'app-survey-viewer',
  standalone: true,
  imports: [CommonModule, SurveyModule],
  template: `
    <div class="viewer-container">
      <h2>Survey Viewer / Visor de Encuestas</h2>
      <p class="instructions">
        Complete the survey below. All fields are required unless otherwise noted.
      </p>
      <p class="instructions">
        Completa la encuesta a continuación. Todos los campos son obligatorios a menos que se indique lo contrario.
      </p>

      <div *ngIf="loading" class="loading">
        Loading survey... / Cargando encuesta...
      </div>

      <div *ngIf="error" class="error-message">
         error 
      </div>

      <div *ngIf="survey && !loading" class="survey-wrapper">
        <survey [model]="survey"></survey>
      </div>

      <div *ngIf="message" [class]="'message ' + messageType">
         message 
      </div>
    </div>
  `,
  styles: [`
    .viewer-container 
      padding: 20px;
    

    h2 
      color: #1976d2;
      margin-bottom: 10px;
    

    .instructions 
      color: #666;
      margin-bottom: 15px;
      line-height: 1.6;
    

    .loading 
      text-align: center;
      padding: 40px;
      color: #666;
      font-size: 18px;
    

    .error-message 
      padding: 15px;
      background-color: #f8d7da;
      color: #721c24;
      border: 1px solid #f5c6cb;
      border-radius: 4px;
      margin: 20px 0;
    

    .survey-wrapper 
      margin: 20px 0;
    

    .message 
      margin-top: 15px;
      padding: 12px;
      border-radius: 4px;
    

    .message.success 
      background-color: #d4edda;
      color: #155724;
      border: 1px solid #c3e6cb;
    

    .message.error 
      background-color: #f8d7da;
      color: #721c24;
      border: 1px solid #f5c6cb;
    
  `]
)
export class SurveyViewerComponent implements OnInit {
  survey: SurveyModel | null = null;
  loading = false;
  error="";
  message="";
  messageType: 'success' | 'error' = 'success';

  constructor(private surveyService: SurveyService) 

  ngOnInit() 
    this.loadSurvey();
  

  /**
   * Load survey from backend
   * Cargar encuesta desde el backend
   */
  loadSurvey() 
    this.loading = true;
    this.error="";
    this.message="";

    this.surveyService.loadSurvey().subscribe(
      next: (response: any) =>  response;
        if (surveyJson && Object.keys(surveyJson).length > 0) 
          this.survey = new SurveyModel(surveyJson);
          this.setupSurveyCompleteHandler();
          console.log('Survey loaded:', surveyJson);
         else 
          this.error="No survey found. Please create one first. / No se encontró encuesta. Por favor crea una primero.";
        
      ,
      error: (error) => 
        this.loading = false;
        console.error('Error loading survey:', error);
        this.error="Error loading survey. / Error al cargar encuesta.";
      
    );
  

  /**
   * Setup survey complete handler after survey is loaded
   * Configurar el manejador de finalización según la forma oficial de SurveyJS
   * Official docs: 
   */
  private setupSurveyCompleteHandler() {
    if (this.survey) {
      // Forma oficial de SurveyJS según la documentación
      this.survey.onComplete.add((sender) => 
        const results = sender.data;
        console.log('Survey completed:', results);

        this.surveyService.saveResults(results).subscribe(
          next: (response) => 
            if (response.success) 
              this.showMessage('Thank you! Your responses have been saved. / ¡Gracias! Tus respuestas han sido guardadas.', 'success');
             else 
              this.showMessage('Error saving results / Error al guardar resultados', 'error');
            
          ,
          error: (error) => 
        );
      );
    }
  }

  private showMessage(text: string, type: 'success' | 'error') 
    this.message = text;
    this.messageType = type;
    if (type === 'success') 
      setTimeout(() => 
        this.message="";
      , 5000);
    
  
}

Kustomisasi tema dan visual

SurveyJS menyediakan sistem tema yang memungkinkan kustomisasi lengkap tampilan dan nuansa survei Anda. Tema khusus dapat digunakan untuk mempertahankan identitas visual yang konsisten dengan merek organisasi Anda.

Integrasi penuh: alur kerja dan layanan

Layanan sudut untuk komunikasi backend

Dalam aplikasi web modern yang berinteraksi dengan data eksternal, penting untuk menjaga pemisahan yang jelas antara antarmuka pengguna dan logika back-end. Dalam platform manajemen formulir ini, lapisan layanan Angular bertindak sebagai perantara antara front-end SurveyJS dan API back-end PHP. Pemisahan ini membuat aplikasi tetap modular, dapat diuji, dan lebih mudah dipelihara.

Layanan Angular mengelola semua komunikasi dengan backend PHP. Mereka menyediakan lapisan abstraksi yang menyederhanakan operasi buat, baca, perbarui, dan hapus (CRUD) sekaligus menangani otentikasi bila diperlukan.

Layanan utama dari proyek ini adalah SurveyServiceberbaring src/app/survey.service.ts. Layanan ini bertanggung jawab atas semua interaksi klien-server yang terkait dengan survei. Secara internal, ia menggunakan Angular HttpClient Berkomunikasi dengan titik akhir REST yang ditentukan di backend PHP, misalnya:

  • /api/load_survey.php
  • /api/save_survey.php
  • /api/save_results.php

Tanggung jawab utama SurveyService termasuk:

  • loadSurvey() – Memuat mode survei dari backend.
  • saveSurvey() – Simpan mode survei dengan mengirimkan data JSON ke save_survey.php.
  • saveResults() – menyimpan tanggapan pengguna di survey_results.json.
  • loadResults() – Ambil semua akumulasi respons survei untuk dianalisis.

Layanan ini menyediakan antarmuka yang sederhana dan efisien untuk mengelola semua operasi investigasi tanpa memerlukan logika otentikasi atau verifikasi yang rumit. Hal ini memungkinkan front-end untuk berinteraksi dengan back-end secara efisien sambil menjaga kode tetap modular dan dapat dipelihara.

File: src/app/survey.service.ts

import  Injectable  from '@angular/core';
import  HttpClient  from '@angular/common/http';
import  Observable  from 'rxjs';

export interface SurveySchema 
  pages?: any[];
  questions?: any[];
  [key: string]: any;


export interface SurveyResult 
  timestamp: string;
  results: any;


@Injectable(
  providedIn: 'root'
)
export class SurveyService 
  // API URL - Adjust if your Laragon setup uses a different path
  private apiUrl="/api";

  constructor(private http: HttpClient) 

  /**
   * Save survey schema to backend
   */
  saveSurvey(survey: SurveySchema): Observable<any> 
    return this.http.post(`$this.apiUrl/save_survey.php`,  survey );
  

  /**
   * Load survey schema from backend
   */
  loadSurvey(): Observable<SurveySchema> 
    return this.http.get<SurveySchema>(`$this.apiUrl/load_survey.php`);
  

  /**
   * Save survey results
   */
  saveResults(results: any): Observable<any> 
    const data: SurveyResult = 
      timestamp: new Date().toISOString(),
      results
    ;
    return this.http.post(`$this.apiUrl/save_results.php`, data);
  

  /**
   * Load survey results
   */
  loadResults(): Observable<SurveyResult[]> 
    return this.http.get<SurveyResult[]>(`$this.apiUrl/load_results.php`);
  

Backend PHP: arsitektur langsung dan aliran komunikasi

Backend sebagai sistem pemrosesan

Backend PHP bertanggung jawab atas semua pemrosesan data platform manajemen formulir. Ia menerima permintaan dari frontend Angular, memprosesnya, dan mengembalikan respons terstruktur. Backend menggunakan model titik akhir langsung yang berfokus pada kesederhanaan dan fungsionalitas langsung daripada arsitektur multi-tingkat yang kompleks.

Lapisan 1: Antarmuka komunikasi dan konfigurasi CORS

Teknis implementasi

Lapisan pertama membuat komunikasi HTTP dan mengonfigurasi kebijakan Cross-Origin Resource Sharing (CORS). Lapisan ini diimplementasikan melalui empat file PHP utama, yang berfungsi sebagai titik masuk ke sistem:

  • api/load_survey.php – Pemuatan mode investigasi
    • Tetapkan header CORS untuk pengembangan lokal
    • Konfigurasi Content-Type tiba application/json
    • Verifikasi metode HTTP GET
    • membaca survey_schema.json dari data/ Daftar isi
    • Periksa integritas JSON sebelum mengembalikannya
  • api/save_survey.php – Penghematan pola
    • Atur header keamanan dan CORS
    • Verifikasi metode HTTP POST
    • Memproses input JSON dan memvalidasinya
    • otomatis dibuat data/ Jika direktori tidak ada
    • Tulis file menggunakan format yang dapat dibaca dan dukungan Unicode

Lapisan 2: Penanganan kesalahan dan validasi dasar

titik akhir penyimpanan

  • api/save_results.php – Penyimpanan respons
    • Verifikasi struktur data yang masuk
    • Baca hasil yang ada untuk mempertahankan akumulasi respons
    • Tambahkan respons baru dengan stempel waktu otomatis
    • Menjaga integritas data yang ada
    • Mengembalikan jumlah total komitmen
  • api/load_results.php – Pencarian hasil
    • Tangani file yang tidak ada dengan mengembalikan array kosong
    • Verifikasi integritas JSON
    • Normalisasikan data ke dalam format array yang konsisten
    • Lakukan validasi struktural pada data yang dikembalikan

Pemrosesan langsung

Simpan rencana survei

Permintaan masuk: POST /api/save_survey.php Dengan skema survei JSON

Proses langkah demi langkah:

  1. antarmuka(api/save_survey.php)
  • Parsing header HTTP dan konfigurasikan CORS
  • Verifikasi apakah metode permintaannya adalah POST
  • Baca data JSON dari php://input
  1. Verifikasi (file yang sama)
  • Periksa sintaks JSON
  • Verifikasi struktur yang diperlukan (data['survey'])
  • Pastikan bidang bukan nol
  1. Persistensi (file yang sama)
  • membuat data/ Jika direktori tidak ada
  • Menulis survey_schema.json menggunakan JSON_PRETTY_PRINT
  • Mengembalikan konfirmasi termasuk informasi file dan byte yang ditulis

membalas:

 "success": true, "message": "...", "file": "...", "bytes_written": ... 

Simpan respons pengguna

Permintaan masuk: POST /api/save_results.php dengan tanggapan survei

Proses langkah demi langkah:

  1. antarmuka(api/save_results.php)
  • Konfigurasikan header HTTP
  • Metode permintaan verifikasi
  • Memproses masukan JSON
  1. Verifikasi (file yang sama)
  • Verifikasi struktur hasil yang diperlukan
  • Verifikasi stempel waktu, buat stempel waktu jika tidak ada
  1. Kumulatif (file yang sama)
  • membaca yang ada survey_results.json
  • Tambahkan balasan baru
  • Simpan balasan sebelumnya
  1. Persistensi (file yang sama)
  • Tulis array yang diperbarui ke file
  • Mengembalikan respons termasuk jumlah total penerapan

membalas:

 "success": true, "message": "...", "total_submissions": ..., "bytes_written": ... 

Alur kerja dan arsitektur lengkap

Sebagai suatu sistem yang terintegrasi, platform survei memiliki struktur sebagai berikut:

Frontend Angular ←→ PHP API Endpoints ←→ JSON Files (data/)
       ↑               ↑
   SurveyJS         Direct Endpoints
Components +       survey_schema.json
SurveyService +    survey_results.json
CORS Configured

Langkah-langkah alur kerja:

1. Penciptaan:

Survei Desain Pengguna → SurveyCreator → save_survey.phpdata/survey_schema.json

2. Visualisasi:

Beban aplikasi→ load_survey.php → SurveyViewer merender survei

3. Pengumpulan balasan:

Pengguna menyelesaikan survei → SurveyService.saveResults()save_results.phpdata/survey_results.json

4. Analisis:

Permintaan aplikasi→ load_results.php → Pemrosesan array respons

sebagai kesimpulan

Kombinasi Angular 17 dan SurveyJS memberikan landasan yang stabil untuk membangun platform manajemen formulir yang kuat. Arsitektur titik akhir langsung menunjukkan bahwa kesederhanaan desain tidak mengurangi fungsionalitas.

Sistem ini secara efisien menangani pembuatan survei, presentasi, respons pengguna, dan penyimpanan hasil, namun tetap mudah dipelihara. Hal ini juga mengatasi keterbatasan alat survei SaaS, memberikan organisasi kontrol penuh atas data mereka dan fleksibilitas untuk menerapkan penyesuaian apa pun pada UI dan formulir pembuat formulir.

Dengan menggunakan arsitektur sederhana dengan titik akhir yang jelas, platform ini memungkinkan fungsionalitas praktis tanpa kerumitan yang tidak perlu.


rencana pengembangan website


metode pengembangan website

jelaskan beberapa rencana untuk pengembangan website, proses pengembangan website, kekuatan dan kelemahan bisnis pengembangan website , jasa pengembangan website, tahap pengembangan website, biaya pengembangan website

#Bagaimana #saya #membangun #platform #manajemen #formulir #menggunakan #Angular #dan #SurveyJS

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *