@libs-ui/components-preview-text-data
v0.2.357-8
Published
> Code editor/viewer mạnh mẽ dựa trên CodeMirror 6, hỗ trợ highlight cú pháp đa ngôn ngữ, linter thời gian thực, IntelliSense và bảo mật pattern.
Downloads
2,585
Readme
@libs-ui/components-preview-text-data
Code editor/viewer mạnh mẽ dựa trên CodeMirror 6, hỗ trợ highlight cú pháp đa ngôn ngữ, linter thời gian thực, IntelliSense và bảo mật pattern.
Giới thiệu
LibsUiComponentsPreviewTextDataComponent cung cấp một trình soạn thảo/xem mã nguồn tích hợp sẵn vào Angular với kiến trúc Signals và OnPush. Component hỗ trợ highlight cú pháp cho 14 ngôn ngữ lập trình, kiểm tra lỗi cú pháp thời gian thực (linter) cho JavaScript, JSON, SQL và Python, gợi ý code thông minh (IntelliSense với dot-notation), cùng cơ chế bảo mật chặn pattern nguy hiểm cả khi gõ lẫn paste.
Tính năng
- Highlight cú pháp cho 14 ngôn ngữ: JavaScript/TypeScript, HTML, CSS/SCSS, JSON, SQL, XML, YAML, Python, Java, C/C++, PHP, Go, Markdown, Plain Text
- Linter tích hợp phát hiện lỗi cú pháp thời gian thực cho JavaScript, JSON, SQL, Python
- IntelliSense với dot-notation: định nghĩa object/biến sẵn, gợi ý properties/methods khi gõ dấu chấm (giống IDE)
- Security policy: chặn pattern nguy hiểm (string hoặc RegExp) ngay khi gõ, chặn paste chứa vi phạm, emit danh sách vi phạm qua
(outViolations) - Lazy load ngôn ngữ: chỉ tải extension khi cần, tối ưu bundle size
- Non-destructive update: đổi ngôn ngữ, wrap, completions không làm mất nội dung hay vị trí con trỏ
- Chế độ editable/readonly linh hoạt
- Thanh công cụ tích hợp: copy nội dung, toggle line wrap, dropdown chọn ngôn ngữ
- Two-way binding cho
langSelectedquamodel() - Standalone component, ChangeDetectionStrategy.OnPush, Angular Signals
Khi nào sử dụng
- Hiển thị cấu hình JSON, YAML hoặc đoạn mã mẫu trong tài liệu hoặc dashboard
- Cho phép người dùng biên tập câu lệnh SQL hoặc mã JavaScript trong các công cụ quản trị
- Xây dựng low-code/no-code editor cần kiểm soát bảo mật (chặn eval, fetch, localStorage...)
- Kiểm tra và xem log hoặc raw data từ API với highlight cú pháp rõ ràng
- Tích hợp IntelliSense cho đối tượng domain cụ thể (biến, API object, schema) trong form nhập mã
Cài đặt
npm install @libs-ui/components-preview-text-dataImport
import { LibsUiComponentsPreviewTextDataComponent } from '@libs-ui/components-preview-text-data';
@Component({
standalone: true,
imports: [LibsUiComponentsPreviewTextDataComponent],
// ...
})
export class MyComponent {}Import thêm types, constants khi cần:
import {
LibsUiComponentsPreviewTextDataComponent,
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
IPreviewTextDataChange,
IPreviewTextDataCompletionItem,
IPreviewTextDataViolation,
ILanguageOptions,
DEFAULT_SECURITY_PATTERNS,
} from '@libs-ui/components-preview-text-data';Ví dụ sử dụng
1. Chế độ xem (Read-only) — ẩn thanh công cụ
// my-viewer.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LibsUiComponentsPreviewTextDataComponent } from '@libs-ui/components-preview-text-data';
@Component({
selector: 'app-my-viewer',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPreviewTextDataComponent],
template: `
<libs_ui-components-preview_text_data
[content]="configCode"
[langSelected]="'json'"
[hiddenAction]="true"
background="#fcfcfc" />
`,
})
export class MyViewerComponent {
readonly configCode = `{
"projectName": "Libs UI",
"version": "2.0.0",
"features": ["Signals", "CodeMirror", "OnPush"]
}`;
}2. Chế độ chỉnh sửa với Linter và đổi ngôn ngữ
// sql-editor.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
LibsUiComponentsPreviewTextDataComponent,
IPreviewTextDataChange,
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
} from '@libs-ui/components-preview-text-data';
import { Diagnostic } from '@codemirror/lint';
@Component({
selector: 'app-sql-editor',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPreviewTextDataComponent],
template: `
<libs_ui-components-preview_text_data
[content]="sqlQuery"
[(langSelected)]="currentLang"
[editable]="true"
[langsAccept]="['sql', 'javascript', 'json']"
(outChange)="handlerChange($event)"
(syntaxErrors)="handlerSyntaxErrors($event)" />
@if (lintErrors().length) {
<div class="p-3 bg-red-50 text-red-600 text-xs font-mono mt-2 rounded">
Lỗi cú pháp: {{ lintErrors()[0].message }}
</div>
}
`,
})
export class SqlEditorComponent {
readonly currentLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('sql');
readonly lintErrors = signal<Diagnostic[]>([]);
readonly sqlQuery = `SELECT id, name, email
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 50;`;
handlerChange(event: IPreviewTextDataChange): void {
event.stopPropagation?.();
console.log('Nội dung thay đổi:', event.content, '— ngôn ngữ:', event.language);
}
handlerSyntaxErrors(errors: Diagnostic[]): void {
this.lintErrors.set(errors);
}
}3. IntelliSense với dot-notation (giống IDE)
// js-intellisense.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
LibsUiComponentsPreviewTextDataComponent,
IPreviewTextDataCompletionItem,
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
} from '@libs-ui/components-preview-text-data';
@Component({
selector: 'app-js-intellisense',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPreviewTextDataComponent],
template: `
<libs_ui-components-preview_text_data
[content]="starterCode"
[(langSelected)]="lang"
[editable]="true"
[completions]="completions"
background="#fafbfc" />
`,
})
export class JsIntelliSenseComponent {
readonly lang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
readonly starterCode = `// Gõ "user." hoặc "config." để xem gợi ý
function process() {
const name = user.
const url = config.
}`;
readonly completions: IPreviewTextDataCompletionItem[] = [
{
label: 'user',
type: 'variable',
detail: 'object',
info: 'User object — thông tin người dùng hiện tại',
properties: [
{ label: 'id', type: 'property', detail: 'number', info: 'Unique user ID' },
{ label: 'name', type: 'property', detail: 'string', info: 'Họ và tên đầy đủ' },
{ label: 'email', type: 'property', detail: 'string', info: 'Địa chỉ email' },
{ label: 'role', type: 'property', detail: "'admin' | 'user'", info: 'Vai trò trong hệ thống' },
{ label: 'logout', type: 'method', detail: '() => Promise<void>', info: 'Đăng xuất tài khoản' },
],
},
{
label: 'config',
type: 'variable',
detail: 'object',
info: 'Application configuration',
properties: [
{ label: 'apiUrl', type: 'property', detail: 'string', info: 'Base URL của API server' },
{ label: 'timeout', type: 'property', detail: 'number', info: 'Request timeout (ms)' },
{ label: 'debug', type: 'property', detail: 'boolean', info: 'Bật debug logging' },
],
},
];
}4. Security policy — chặn pattern nguy hiểm
// secure-editor.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
LibsUiComponentsPreviewTextDataComponent,
DEFAULT_SECURITY_PATTERNS,
IPreviewTextDataViolation,
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
} from '@libs-ui/components-preview-text-data';
@Component({
selector: 'app-secure-editor',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPreviewTextDataComponent],
template: `
<libs_ui-components-preview_text_data
[content]="starterCode"
[(langSelected)]="lang"
[editable]="true"
[forbiddenPatterns]="securityPatterns"
(outViolations)="handlerViolations($event)"
background="#fafbfc" />
@if (violations().length) {
<div class="p-3 bg-red-50 border border-red-200 rounded mt-2">
<p class="text-sm font-semibold text-red-700 mb-1">
Phát hiện {{ violations().length }} vi phạm bảo mật:
</p>
@for (v of violations(); track v.from) {
<p class="text-xs text-red-600 font-mono">
Dòng {{ v.line }}: "{{ v.matched }}" — vi phạm pattern {{ v.pattern }}
</p>
}
</div>
}
`,
})
export class SecureEditorComponent {
readonly lang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
readonly violations = signal<IPreviewTextDataViolation[]>([]);
readonly securityPatterns = DEFAULT_SECURITY_PATTERNS;
readonly starterCode = `// Code thuần — không gọi API hay truy cập storage
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
// Thử gõ: eval("test") hoặc localStorage.getItem('key')
// → sẽ bị highlight đỏ và chặn paste`;
handlerViolations(violations: IPreviewTextDataViolation[]): void {
this.violations.set(violations);
}
}5. Chỉ cho phép một ngôn ngữ cố định (không hiển thị dropdown)
<!-- Khi langsAccept có đúng 1 phần tử trùng với langSelected → dropdown bị ẩn -->
<libs_ui-components-preview_text_data
[content]="jsonData"
[(langSelected)]="lang"
[editable]="true"
[langsAccept]="['json']" />6. Màn nhập code (min/max số dòng + giới hạn ngôn ngữ)
// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsPreviewTextDataComponent, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
@Component({
selector: 'app-code-input',
standalone: true,
imports: [LibsUiComponentsPreviewTextDataComponent],
template: `
<libs_ui-components-preview_text_data
[content]="''"
[(langSelected)]="lang"
[editable]="true"
[langsAccept]="['javascript', 'python']"
[minLines]="10"
[maxLines]="20"
[zIndexPopover]="1500" />
`,
})
export class CodeInputComponent {
// Mặc định trống, chỉ cho JS/Python; editor hiện sẵn tối thiểu 10 dòng, vượt 20 dòng → xuất hiện scroll.
readonly lang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
}7. Editor tự fit theo container (min/max-height)
Thay vì truyền minLines/maxLines, bọc editor trong 1 div có min-height/max-height rồi truyền chính element đó qua [containerElement]. Editor tự tính số dòng và snap chiều cao theo container. Không dùng chung với minLines/maxLines (sẽ throw lỗi).
<!-- #boxRef là template reference tới div container -->
<div #boxRef class="min-h-[120px] max-h-[300px]">
<libs_ui-components-preview_text_data
[content]="''"
[(langSelected)]="lang"
[editable]="true"
[langsAccept]="['javascript', 'python']"
[containerElement]="boxRef" />
</div>@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [content] | string | '' | Nội dung code cần hiển thị hoặc chỉnh sửa | [content]="myCode" |
| [(langSelected)] | PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT | Bắt buộc | Ngôn ngữ highlight hiện tại. Hỗ trợ two-way binding qua model() | [(langSelected)]="currentLang" |
| [editable] | boolean | false | Cho phép người dùng chỉnh sửa nội dung trong editor | [editable]="true" |
| [hiddenAction] | boolean | false | Ẩn toàn bộ thanh công cụ (Copy, Wrap, dropdown ngôn ngữ) | [hiddenAction]="true" |
| [langsAccept] | PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT[] | undefined | Danh sách ngôn ngữ được phép chọn trong dropdown. Nếu chỉ có 1 phần tử trùng langSelected → dropdown bị ẩn | [langsAccept]="['json', 'sql']" |
| [langsChangeLabel] | ILanguageOptions | undefined | Tùy chỉnh nhãn hiển thị của một ngôn ngữ trong dropdown | [langsChangeLabel]="{ id: 'json', label: 'Cấu hình JSON' }" |
| [completions] | IPreviewTextDataCompletionItem[] | [] | Danh sách object/biến gợi ý khi gõ — hỗ trợ dot-notation cho properties lồng nhau (IntelliSense) | [completions]="myCompletions" |
| [forbiddenPatterns] | T_ForbiddenPattern[] | [] | Danh sách pattern bị cấm (string hoặc RegExp). Highlight đỏ khi gõ, chặn paste chứa vi phạm, phát ra (outViolations) | [forbiddenPatterns]="DEFAULT_SECURITY_PATTERNS" |
| [lintIgnorePatterns] | string[] | ['Cannot use import statement...', 'Unexpected token export', 'import ', '@angular/core'] | Danh sách chuỗi lỗi linter JS sẽ bị bỏ qua (ignore) trong quá trình kiểm tra | [lintIgnorePatterns]="['import ']" |
| [background] | string | '#f8f9fa' | Màu nền của vùng soạn thảo (CSS color value) | background="#ffffff" |
| [minLines] | number | undefined | Số dòng tối thiểu. Nếu content ít dòng hơn sẽ tự bù dòng trống (xử lý 1 lần khi khởi tạo editor); đủ/nhiều hơn thì giữ nguyên, không cắt bớt | [minLines]="10" |
| [maxLines] | number | undefined | Số dòng tối đa hiển thị. Vượt quá → editor giới hạn chiều cao và xuất hiện vùng cuộn (đo line-height thật khi khởi tạo) | [maxLines]="20" |
| [zIndexPopover] | number | undefined | z-index cho popover dropdown chọn ngôn ngữ (dùng khi editor nằm trong modal/overlay có z-index cao) | [zIndexPopover]="1500" |
| [containerElement] | HTMLElement | undefined | Element bọc ngoài (đã set min-height/max-height CSS). Editor snap số dòng theo container: min-height → bù dòng trống cho đủ (hiện line-number 1..N), max-height → giới hạn + scroll. CẤM dùng cùng [minLines]/[maxLines] (throw lỗi) | [containerElement]="boxRef" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outChange) | IPreviewTextDataChange | Phát ra khi nội dung thay đổi, người dùng đổi ngôn ngữ hoặc toggle line wrap | handlerChange(event: IPreviewTextDataChange): void { event.stopPropagation?.(); this.code.set(event.content); } | (outChange)="handlerChange($event)" |
| (syntaxErrors) | Diagnostic[] | Phát ra mảng lỗi cú pháp mỗi khi linter chạy. Mảng rỗng khi không có lỗi | handlerSyntaxErrors(errors: Diagnostic[]): void { this.errors.set(errors); } | (syntaxErrors)="handlerSyntaxErrors($event)" |
| (outViolations) | IPreviewTextDataViolation[] | Phát ra danh sách vi phạm (vị trí, dòng, pattern, chuỗi bị match) mỗi khi nội dung thay đổi khi có forbiddenPatterns. Mảng rỗng khi không có vi phạm | handlerViolations(violations: IPreviewTextDataViolation[]): void { this.violations.set(violations); } | (outViolations)="handlerViolations($event)" |
Types & Interfaces
import {
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
IPreviewTextDataChange,
IPreviewTextDataCompletionItem,
IPreviewTextDataViolation,
ILanguageOptions,
T_ForbiddenPattern,
DEFAULT_SECURITY_PATTERNS,
} from '@libs-ui/components-preview-text-data';/** Tất cả ngôn ngữ được hỗ trợ */
export type PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT =
| 'javascript'
| 'html'
| 'css'
| 'markdown'
| 'json'
| 'sql'
| 'xml'
| 'yaml'
| 'python'
| 'java'
| 'cpp'
| 'php'
| 'go'
| 'text';
/** Payload phát ra qua (outChange) */
export interface IPreviewTextDataChange {
content: string;
isWrap: boolean;
language: PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT;
/** Loại thay đổi: nội dung / ngôn ngữ / line wrap */
contextChange: 'content' | 'language' | 'isWrap';
}
/** Một item trong danh sách completions — hỗ trợ dot-notation qua properties[] */
export interface IPreviewTextDataCompletionItem {
label: string;
/** @default 'variable' */
type?: 'variable' | 'property' | 'function' | 'class' | 'method' | 'keyword' | 'text' | 'constant' | 'interface' | 'type' | 'enum' | 'field' | 'namespace' | 'module';
/** Gợi ý type ngắn hiển thị cạnh label, VD: 'string', '() => void' */
detail?: string;
/** Mô tả dài hiển thị trong tooltip */
info?: string;
/** Properties lồng nhau — được gợi ý sau dấu chấm (dot-notation) */
properties?: IPreviewTextDataCompletionItem[];
}
/** Vi phạm được phát hiện trong editor, phát ra qua (outViolations) */
export interface IPreviewTextDataViolation {
/** Vị trí bắt đầu (character offset) */
from: number;
/** Vị trí kết thúc (character offset) */
to: number;
/** Số dòng (1-indexed) */
line: number;
/** Tên/mô tả của pattern đã bị vi phạm */
pattern: string;
/** Đoạn text thực tế bị match */
matched: string;
}
/** Tùy chỉnh label hiển thị của một ngôn ngữ trong dropdown */
export interface ILanguageOptions {
id: PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT;
label: string;
}
/** Pattern cấm: string literal (case-insensitive) hoặc RegExp tùy chỉnh */
export type T_ForbiddenPattern = string | RegExp;Constants
DEFAULT_SECURITY_PATTERNS
Bộ pattern bảo mật mặc định cho low-code platform — chặn các API nguy hiểm:
import { DEFAULT_SECURITY_PATTERNS } from '@libs-ui/components-preview-text-data';
// Sử dụng trực tiếp
[forbiddenPatterns]="DEFAULT_SECURITY_PATTERNS"Các pattern được chặn:
| Pattern | Lý do |
|---|---|
| /eval\s*\(/i | Thực thi code động |
| /new\s+Function\s*\(/i | Tạo hàm từ string |
| /document\.cookie/i | Đọc cookie nhạy cảm |
| /localStorage/i | Truy cập local storage |
| /sessionStorage/i | Truy cập session storage |
| /fetch\s*\(/i | Gọi HTTP request |
| /new\s+XMLHttpRequest/i | Gọi HTTP request cũ |
| /require\s*\(/i | Load module Node.js |
| /import\s*\(/i | Dynamic import |
| /process\.env/i | Đọc biến môi trường |
| /<script/i | Chèn thẻ script |
| /document\.write\s*\(/i | Ghi trực tiếp vào DOM |
| /window\.location\s*=/i | Chuyển hướng trang |
Ngôn ngữ được hỗ trợ
| ID | Nhãn hiển thị | Linter |
|---|---|---|
| text | Plain Text | Không |
| javascript | JavaScript/TypeScript | Có (JS syntax + trailing dot detection) |
| html | HTML | Không |
| css | CSS/SCSS/SASS | Không |
| markdown | Markdown | Không |
| json | JSON | Có (JSON.parse validation) |
| sql | SQL | Có (syntaxTree parse errors) |
| xml | XML | Không |
| yaml | YAML | Không |
| python | Python | Có (syntaxTree parse errors) |
| java | Java | Không |
| cpp | C/C++ | Không |
| php | PHP | Không |
| go | Go | Không |
Lưu ý quan trọng
⚠️ langSelected là bắt buộc: Input [(langSelected)] là model.required() — PHẢI truyền giá trị, không có default. Thiếu giá trị sẽ gây lỗi Angular runtime.
⚠️ Lazy load ngôn ngữ: Component lazy-load extension của từng ngôn ngữ khi cần. Lần đầu tiên chọn một ngôn ngữ mới có thể có độ trễ nhỏ (vài trăm ms) để tải thư viện từ bundle.
⚠️ Linter chỉ hoạt động khi editable: Linter phát hiện lỗi cú pháp ngay cả khi [editable]="false", nhưng người dùng chỉ nhập được khi [editable]="true". Output (syntaxErrors) và (outViolations) luôn phát ra dù ở chế độ nào.
⚠️ forbiddenPatterns chặn paste hoàn toàn: Khi [forbiddenPatterns] được cấu hình, mọi thao tác paste chứa pattern bị cấm sẽ bị hủy toàn bộ (không paste được phần nào). Người dùng cần paste từng phần hoặc nhập thủ công.
⚠️ Dropdown ngôn ngữ bị ẩn tự động: Khi [langsAccept] chứa đúng 1 ngôn ngữ trùng với langSelected hiện tại, dropdown chọn ngôn ngữ sẽ bị ẩn tự động (computed acceptChangeLang = false).
⚠️ Cleanup tự động: Component tự hủy EditorView instance và xóa cache ngôn ngữ khi bị destroy (qua DestroyRef). Không cần thao tác cleanup thủ công từ phía consumer.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/preview-text-data
Unit Tests
# Chạy test cho lib
npx nx test components-preview-text-data
# Chạy test một file cụ thể
npx nx test components-preview-text-data --testFile=libs-ui/components/preview-text-data/src/preview-text-data.component.spec.ts
# Coverage
npx nx test components-preview-text-data --coverage