npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@libs-ui/services-grid-layout

v0.2.357-31

Published

Mặt phẳng kéo–thả. Thư viện lo **toàn bộ** việc vẽ lưới, kéo thả, đổi kích thước, lồng container, tay cầm kéo, bộ nút trên khối, viền chế độ sửa, và dọn dẹp khi component chết.

Downloads

1,460

Readme

@libs-ui/services-grid-layout

Mặt phẳng kéo–thả. Thư viện lo toàn bộ việc vẽ lưới, kéo thả, đổi kích thước, lồng container, tay cầm kéo, bộ nút trên khối, viền chế độ sửa, và dọn dẹp khi component chết.

Nơi dùng chỉ đưa cây dữ liệucách phân giải component nội dung. Component nội dung của bạn chỉ vẽ phần nội dung — không phải tự lo kích thước, không phải tự vẽ viền chế độ sửa.

Cài

Lib dùng angular-gridster2 qua alias npm để chạy song song nhiều bản (giống cách repo đang chạy quill 1.3.7 và quill2x 2.0.3):

// package.json của nơi dùng
"gridster18": "npm:[email protected]"

Nơi dùng đang có angular-gridster2 bản khác thì không bị đụng — hai bản nằm ở hai thư mục node_modules riêng.

Chạy Jest thì phải map cả hai tên, vì package tự khai name: angular-gridster2:

// jest.config.ts của nơi dùng
moduleNameMapper: {
  '^gridster18$': '<rootDir>/../../node_modules/gridster18/fesm2022/angular-gridster2.mjs',
  '^angular-gridster2$': '<rootDir>/../../node_modules/gridster18/fesm2022/angular-gridster2.mjs',
}

Dùng

@Component({
  imports: [LibsUiGridLayoutCanvasComponent],
  // 🔴 Khai trong providers của CHÍNH component — service theo từng mặt phẳng, không phải root.
  providers: [LibsUiGridLayoutService],
  template: `<libs_ui-services-grid_layout-canvas
    (outChange)="handlerChange($event)"
    (outRemoveNode)="handlerRemoveNode($event)"
    (outAddBlockInside)="handlerAddInside($event)"
    (outToggleType)="handlerToggleType($event)" />`,
})
export class MyPageComponent {
  private readonly gridLayout = inject(LibsUiGridLayoutService);

  constructor() {
    this.gridLayout.init({
      data: layoutFromServer(),          // cây khối, thường lấy từ backend
      getComponentOutlet: resolveNodeComponent,
      mode: E_gridLayout_mode_string.grid,
      isEditMode: true,
    });
  }
  // KHÔNG cần ngOnDestroy — thư viện tự dọn qua DestroyRef.
}

Không truyền [node]init() đã giữ cây. Input node/depth của canvas chỉ dành cho lưới lồng bên trong thư viện.

Phân giải component theo từng khối

Mỗi khối tự quyết định component của nó, đọc từ node.data (dữ liệu hình dạng server). Trả Observable nên import() động dùng được ngay — mỗi loại khối là một chunk riêng:

export const resolveNodeComponent = (node: T_gridLayout_node): Observable<Type<unknown>> => {
  switch ((node.data as T_serverElement | undefined)?.type) {
    case E_serverElement_type_string.chart:
      return from(import('./chart-card.component').then((c) => c.ChartCardComponent));
    case E_serverElement_type_string.table:
      return from(import('./table-card.component').then((c) => c.TableCardComponent));
    default:
      return from(import('./sample-card.component').then((c) => c.SampleCardComponent));
  }
};

Khai ở config.getComponentOutlet cho cả mặt phẳng, hoặc ở node.getComponentOutlet cho riêng một khối (khối tự khai thì thắng).

Component nội dung

Khai đủ bốn input — thư viện setInput đúng bốn tên này, thiếu một cái là lỗi lúc chạy mà build không bắt được:

@Component({ changeDetection: ChangeDetectionStrategy.OnPush, /* … */ })
export class MyCardComponent {
  readonly data = input.required<T_myData>();       // = node.data — thường chỉ cần cái này
  readonly node = input.required<T_gridLayout_node>();
  readonly isEditMode = input<boolean>(false);
  readonly depth = input<number>(0);
}

Type sẵn có: T_gridLayout_nodeInputs<TData>.

Nút sẵn có bắn event kèm callback

Xoá · gộp · tách · thêm-khối-bên-trong là nút của thư viện. Bấm vào, thư viện chưa làm gì — nó bắn event mang theo confirm(). Nghiệp vụ cần hỏi xác nhận thì mở modal rồi mới gọi; không cần thì gọi luôn; không cho phép thì đừng gọi:

protected handlerRemoveNode(event: T_gridLayout_actionEvent): void {
  if (!event.node.children?.length) {
    event.confirm();                    // khối đơn — xoá luôn
    return;
  }
  this.pendingRemove.set(event);        // khối ghép — hỏi trước, confirm() gọi sau
}

protected handlerAddInside(event: T_gridLayout_addEvent): void {
  // confirm nhận khối mới, trả về khối đã tạo kèm id thư viện sinh
  const added = event.confirm({ cols: 3, rows: 2, data: { type: 'metric' } });
}

| Output | Khi nào bắn | confirm() | |---|---|---| | outChange | người dùng kéo / đổi kích thước — chỉ thao tác thật của người dùng | — | | outRemoveNode | bấm nút xoá | () => boolean | | outToggleType | bấm gộp (khối đơn → container) hoặc tách | () => boolean | | outAddBlockInside | bấm + trên container | (item) => node \| undefined | | outDropNode | vừa thả một thẻ kéo từ ngoài vào | — (khối đã chèn rồi) | | outSelectNode | bấm chọn một khối | — | | outConfigNode | bấm nút bánh răng trên khối | () => true | | outZoomChange | tỷ lệ thu phóng vừa đổi | — |

outChange không bắn khi chính bạn gọi addBlock/removeBlock/compact — bạn đã biết mình vừa làm gì, khỏi phải chống dội.

Hai chế độ

| Chế độ | Khối bám lưới | Chồng lên nhau | Dùng khi | |---|---|---|---| | grid | có | chỉ khi allowOverlap bật hai khối khác layerIndex | dashboard, bố cục dạng bảng | | free | không, toạ độ px | tự do | bố cục kiểu canvas — engine chưa dựng, hiện vẫn chạy trên lưới |

API

LibsUiGridLayoutService — một thực thể cho mỗi mặt phẳng.

| Thành viên | Việc | |---|---| | init(config) | dựng mặt phẳng; gọi lại sẽ dọn sạch rồi dựng lại. Tự san phẳng container lồng quá trần | | setEditMode(value) | bật/tắt chế độ sửa | | setAllowOverlap(value) | cho phép xếp chồng; tự gán layerIndex. KHÔNG dựng lại component nào | | refresh(event?) | báo cây vừa đổi từ bên ngoài — hiếm khi cần | | addBlock(item, parentId?) | thêm khối; bỏ parentId là thêm ở gốc. Trả khối vừa tạo (kèm id) | | removeBlock(id) | bỏ khối; xoá container là xoá cả ruột | | toContainer(id) · toSingle(id) | gộp thành container / tách về khối đơn — không mất dữ liệu | | compact() | dồn khối lên, bỏ khoảng trống. Đệ quy vào container | | findBlock(id) · findParentBlock(id) | tra cây | | exportLayout() | cây hiện tại đã bỏ hàm, cloneDeep — gửi thẳng lên backend được | | insertBlockWithCollision(item, col, row, parentId?) | chèn vào ĐÚNG ô và đẩy khối va chạm xuống. addBlock thì tự tìm hàng trống kế tiếp | | duplicateBlock(id, transformData?) | nhân bản; transformData để bạn tự đổi dữ liệu của mình (thư viện không đoán trường nào là tiêu đề) | | resizeBlock(id, { cols, rows }) | đổi kích thước bằng code; rộng quá mép phải thì tự dịch sang trái | | setDragPayload(p) · clearDragPayload() | báo cho canvas biết đang kéo thẻ gì từ palette ngoài vào | | setZoom(v) · zoomIn() · zoomOut() · resetZoom() · fitToScreen(w) | thu phóng | | Root · IsEditMode · Version · Zoom | signal chỉ đọc | | totalColumns | số cột thật của lưới cấp trang (mặc định 12) |

Mọi hàm sửa cây tự vẽ lại — không phải gọi refresh() theo sau.

Thư viện tự dọn qua DestroyRefdestroy()private, CẤM gọi trong ngOnDestroy.

KHÔNG tự đặt id cho khối: addBlock sinh bằng uuid() và trả về khối vừa tạo. Tự đặt là mời trùng id, mà trùng thì findBlock trả nhầm khối và cây hỏng im lặng.

LibsUiGridLayoutTreeService cố ý không xuất: mọi thao tác trên cây đã có trên service với chữ ký gọn hơn (không phải truyền root — thư viện đang giữ nó). Cấu hình gridster cũng không xuất, xem lý do ở mục dưới.

Lưu và mở lại bố cục

UtilsCache.Set(KEY, this.gridLayout.exportLayout(), UtilsCache.CACHE_EXPIRE_NONE);
// mở lại
this.gridLayout.init({ data: UtilsCache.Get(KEY) ?? defaultLayout(), getComponentOutlet });

exportLayout() đã lược getComponentOutlet (hàm không tuần tự hoá được) nên JSON.stringify được ngay. Mở lại chỉ cần đưa getComponentOutlet vào init — cây không mang hàm.

Kéo thẻ từ palette bên ngoài vào

Nơi dùng chỉ làm hai việc ở thẻ palette; canvas lo phần còn lại.

protected handlerPaletteDragStart(event: DragEvent, item: T_paletteItem): void {
  this.gridLayout.setDragPayload({
    type: item.type,
    title: item.label,    // chữ trên khung xem trước
    cols: item.cols,      // thư viện đo khung xem trước theo hai số này
    rows: item.rows,
    // Khối THẬT sẽ tạo. `isInside` = con trỏ đang trong một khối ghép ⇒ thu cho vừa ruột
    // container (lưới con luôn 12 cột).
    resolveNode: (isInside: boolean) => ({
      cols: isInside ? 12 : item.cols,
      rows: isInside ? 4 : item.rows,
      data: { ...buildBlockData(item), label: '' },   // nhãn TRỐNG — đánh số ở `outDropNode`
    }),
  });
  event.dataTransfer?.setData('text/plain', item.label);   // thiếu dòng này Firefox không kéo được
}

protected handlerPaletteDragEnd(): void {
  this.gridLayout.clearDragPayload();   // người dùng thả RA NGOÀI canvas thì thư viện không bắn gì
}
<div draggable="true"
  (dragstart)="handlerPaletteDragStart($event, item)"
  (dragend)="handlerPaletteDragEnd()">…</div>

<libs_ui-services-grid_layout-canvas (outDropNode)="handlerDropNode($event)" />

🔴 CẤM tự viết (dragover) / (drop) trên canvas. Canvas đã làm: vẽ khung xem trước bám ô lưới, bật ô li của container khi con trỏ đi vào trong rồi tắt đúng lúc, chèn khối qua insertBlockWithCollision và đẩy khối va chạm xuống.

🔴 resolveNode phải THUẦN. Thư viện gọi nó ở mỗi nhịp dragover chỉ để ĐO khung xem trước. Cấm tăng bộ đếm, sinh id, gọi API bên trong — đánh số khối thì đánh ở outDropNode.

🔴 outDropNode không có confirm() — lúc nó bắn thì khối đã chèn rồi. Chặn trước thì chặn ở dragstart; gỡ sau thì removeBlock(event.node.id). event.parentId khác undefined ⇒ người dùng thả vào ruột một khối ghép.

LibsUiGridLayoutDragController là lớp tuỳ chọn cho trường hợp bạn tự dựng bề mặt thả riêng ngoài canvas. Dùng canvas sẵn có thì không cần đụng tới.

Thu phóng và Stage

this.gridLayout.init({
  getComponentOutlet,
  data: layoutFromServer(),
  enableZoom: true,
  stageWidth: 1400,        // 12 cột chia trên con số NÀY, không phải bề rộng cửa sổ
  stageMinHeight: 900,
  zoomMin: 0.4,
  zoomMax: 1.5,
  storageKey: 'bao_cao_ban_hang_zoom',   // 🔴 RIÊNG từng màn
  autoFitOnLoad: true,
});

Bật enableZoom là canvas dựng một stage khổ cố định rồi phóng to/thu nhỏ cả stage — bố cục nhìn giống nhau trên mọi khổ màn. Không bật thì 12 cột chia theo bề rộng cửa sổ, mỗi máy một kích thước khối.

🔴 storageKey phải riêng từng màn — dùng chung thì hai màn ghi đè tỷ lệ của nhau.

Canvas có sẵn cụm nút thu phóng ở góc; muốn đặt nút chỗ khác thì [showZoomDock]="false" rồi gọi zoomIn() · zoomOut() · resetZoom() · setZoom(v) · fitToScreen(width).

Input của canvas

| Input | Mặc định | Việc | |---|---|---| | [(selectedNodeId)] | undefined | khối đang chọn — hai chiều, thư viện ghi id vào signal của bạn | | [(zoom)] | undefined | tỷ lệ thu phóng — hai chiều | | [showGridLines] | false | vẽ ô li của lưới (lúc KÉO thư viện tự bật rồi tự tắt) | | [showZoomDock] | true | cụm nút thu phóng ở góc canvas | | [enableExternalDrop] | true | cho thả thẻ kéo từ ngoài vào | | [hiddenToolbar] | false | ẩn hẳn bộ nút mặc định trên khối | | [templateToolbar] | undefined | tự vẽ thanh công cụ của khối |

🔴 nodedepth là input NỘI BỘ — thư viện tự truyền khi vẽ lưới con. Mặt phẳng gốc đọc cây từ service (đã đưa vào qua init()), truyền thêm là khai hai lần cùng một thứ.

<libs_ui-services-grid_layout-canvas [templateToolbar]="toolbar" />

<ng-template #toolbar let-node
  let-isContainer="isContainer" let-canAddInside="canAddInside"
  let-remove="remove" let-addInside="addInside" let-toggleType="toggleType">
  @if (canAddInside) { <button (click)="addInside()">+</button> }
  <button (click)="toggleType()">{{ isContainer ? 'Tách' : 'Gộp' }}</button>
  <button (click)="remove()">Xoá</button>
</ng-template>

🔴 templateToolbar đổi giao diện, không đổi luật: ba hàm trong context chỉ phát đúng sự kiện (outRemoveNode · outAddBlockInside · outToggleType), y như bấm nút mặc định. Vẫn phải bắt sự kiện rồi gọi event.confirm().

Vì sao cấu hình gridster lại như vậy

defines/gridster-config.define.ts giữ toàn bộ cấu hình đã vá. Mỗi dòng có dấu 🔴 tương ứng một lỗi đã đo được trên sản phẩm thật — đổi nó là lỗi sống lại:

| Cấu hình | Bỏ đi thì gặp lỗi gì | |---|---| | relative trên khối cuộn | offsetParent sai → gridster tính ô lệch ~49px, ô chờ đặt không theo con trỏ | | compactType: None | chỗ vừa nhường bị hút lấp lại → kéo mép bị hoàn nguyên, khối kéo treo lơ lửng | | pushItems: true | khối lệch kích thước không swap được, cũng không nhường đường → bế tắc | | delayStart: 160 | bấm chọn một khối là nó nhích theo dù chuột đứng yên | | dragHandleClass riêng cho lưới con | kéo khối con nhấc luôn container | | maxItemRows/Cols/Area nới | khối bị chặn chiều cao (trần diện tích 2500 khó thấy nhất) | | tay cầm resize ở 0, không âm | overflow: hidden cắt mất nửa tay cầm → kéo mép không ăn | | setGridSize: false ở lưới lồng | vòng lặp đo–ghi, lưới con co dần về 64×64px | | disableScroll* ở lưới lồng | khối con bị đẩy ra ngoài container rồi kẹt | | layerIndex gán vào cây ĐANG VẼ | gán vào config.data thì z-index đứng ở 1 — cây đang vẽ là bản clone |

Kiểm

npx nx lint services-grid-layout
npx nx test services-grid-layout
npx nx build services-grid-layout