Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,44 @@ so it stays clear which part of the repository actually moved.
attempt, so it cannot be retried away. A propagation timeout now says so in those words, to stop a
future reader from re-diagnosing it as the duplicate of issue #211.

### Added

#### flex-fields

- **The `Select` field types now load their `ng-zorro-antd` stylesheet themselves, by bundle name,
exactly the way `abp-tree` loads its own.** ng-zorro-antd ships no component styles, so `<nz-select>`
had been rendering against whatever antd CSS a host happened to have declared. `SelectControlComponent`
and `SelectSearchComponent` now ask for `ng-zorro-antd-select.css` once per application at init; a
host serves it with a single `angular.json` `styles` entry —
`node_modules/ng-zorro-antd/select/style/index.min.css`, `inject: false`,
`bundleName: "ng-zorro-antd-select"` — and can switch the loading off with the new
`DISABLE_FLEX_FIELDS_STYLE_LOADING_TOKEN` when it bundles that CSS another way. A host that has not
declared the entry now gets one console error naming the missing file and quoting the entry to add,
rather than a silently unstyled control. Shipping an aggregated stylesheet inside the package was
considered and rejected on two counts: ng-zorro-antd declares `less` before `style` in its
`./<component>/style/*` export, so a bare `@import 'ng-zorro-antd/select/style/index.min.css'`
resolves to a non-existent `index.min.css.less` under the Angular CLI's stylesheet bundler, and
freezing a copy of a peer dependency's CSS into this package's release cycle is not an acceptable
substitute. The demo's dead `ng-zorro-antd-tree-select` entry went with it — nothing renders
`nz-tree-select`. See the package README's new "Styles" section for the host-side contract. (#232)
- **`@dignite/ng.flex-fields-ckeditor` loads CKEditor 5's stylesheet the same way, through the same
loader.** `FlexFieldsStyleLoader` is shared across the package family; each package declares its own
bundle constant. The bolt-on used to `@import 'ckeditor5/ckeditor5.css'` from its control
component's stylesheet, which ng-packagr inlined at build time: 241 KB of third-party CSS compiled
into the published `fesm2022` bundle (471 KB, 522 `.ck-editor` rules) and, because a host registers
the field type in its application config, shipped in that host's *initial* bundle whether or not a
rich-text field was ever opened — while pinning the CSS to whatever `ckeditor5` version the package
was built against, though the editor's own JavaScript comes from the host's installed copy via
`await import('ckeditor5')`. `CKEditorControlComponent` now asks for the host's `ckeditor5` bundle at
init: one `angular.json` `styles` entry — `node_modules/ckeditor5/dist/ckeditor5.css`,
`inject: false`, `bundleName: "ckeditor5"`, exported as `CKEDITOR5_STYLE`. The bolt-on's bundle drops
to 36 KB and this repo's demo from a 2.20 MB initial bundle to 1.98 MB (483 kB to 455 kB
transferred), back under the 2 MB budget the build had been warning about. A host that has not
declared the entry gets one console error naming the file and quoting the entry, instead of an editor
that silently renders as blank/collapsed space. `DISABLE_FLEX_FIELDS_STYLE_LOADING_TOKEN` is
family-wide: `true` silences every bundle loaded through this service, in every
`@dignite/ng.flex-fields*` package. (#232)

## [10.0.0-rc.16] - 2026-09-05

### Fixed
Expand Down
10 changes: 5 additions & 5 deletions flex-fields/angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,18 @@
},
{
"input": "node_modules/ng-zorro-antd/tree/style/index.min.css",
"inject": true,
"inject": false,
"bundleName": "ng-zorro-antd-tree"
},
{
"input": "node_modules/ng-zorro-antd/select/style/index.min.css",
"inject": true,
"inject": false,
"bundleName": "ng-zorro-antd-select"
},
{
"input": "node_modules/ng-zorro-antd/tree-select/style/index.min.css",
"inject": true,
"bundleName": "ng-zorro-antd-tree-select"
"input": "node_modules/ckeditor5/dist/ckeditor5.css",
"inject": false,
"bundleName": "ckeditor5"
},
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
Expand Down
24 changes: 24 additions & 0 deletions flex-fields/angular/projects/flex-fields-ckeditor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ need a rich-text field never pay for its dependency weight. Install this package
npm install @dignite/ng.flex-fields-ckeditor @ckeditor/ckeditor5-angular ckeditor5 marked
```

### Styles

CKEditor 5's UI stylesheet is **served by your host under a fixed name** and fetched the first time a
`CKEditor` field is rendered, rather than compiled into this package. One entry in the `styles` array
of your `angular.json` build target:

```json
{ "input": "node_modules/ckeditor5/dist/ckeditor5.css", "inject": false, "bundleName": "ckeditor5" }
```

It is the `ckeditor5` you just installed: this package's editor JavaScript also comes from your copy,
at runtime, so declaring the CSS the same way keeps the two halves on one version instead of pinning
the stylesheet to whatever version this package was built against. `inject: false` is required, not a
preference — an injected entry is emitted under a content hash in a production build, which no fixed
name can find; the [core package's README](https://github.com/dignite-projects/abp-modules/blob/main/flex-fields/angular/projects/flex-fields/README.md#styles) has the full explanation,
and that section's `ng-zorro-antd-*` entries apply on top of this one if you also use the built-in
field types.

Without the entry the editor's DOM is still built, but CKEditor's layout never arrives, so the field
renders as blank/collapsed space — and the browser console carries one error naming the file and
quoting the entry to add. `DISABLE_FLEX_FIELDS_STYLE_LOADING_TOKEN` (from `@dignite/ng.flex-fields`)
switches the loading off for an application that already bundles this CSS some other way; it is
family-wide, so `true` silences the sibling packages' bundles too.

## Usage

Register it alongside the built-ins, in your application config:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
/* CKEditor 5's own UI framework CSS. Without this, the toolbar/editable DOM CKEditor builds at
runtime is completely unstyled and typically renders as blank/collapsed space - no console error,
since nothing is actually broken, it just has no visual layout. Component-scoped rather than
pushed onto every consumer's global styles.css, which is easy to forget (this is a fix for exactly
that oversight) - see the ViewEncapsulation.None on this component's @Component decorator, required
because CKEditor attaches balloons/dropdowns outside this component's own template subtree, where
Angular's default per-component style scoping would not reach them. */
@import 'ckeditor5/ckeditor5.css';

/* CKEditor 5's balloon/dropdown panels (InlineEditor's floating toolbar, link/table pickers, etc.)
attach to a wrapper appended directly under <body> (see the import's own comment) and default to
z-index 1000 (--ck-z-default: 1, --ck-z-panel: default + 999). ng-bootstrap's modal window sits at
1055 - higher - so any of those panels opened while a CKEditor is inside an <abp-modal> or similar
ng-bootstrap dialog render fully in-DOM and CSS-"visible", but painted underneath the modal, i.e.
invisible to the user despite every element reporting visible/opacity:1. Raising the base above any
Bootstrap-derived dialog z-index (Bootstrap's own scale tops out at 1080) fixes every derived panel
at once; harmless when no modal is present since there is nothing competing for the stacking order. */
/* CKEditor 5's own UI framework CSS is deliberately NOT part of this file. It used to be - an
`@import 'ckeditor5/ckeditor5.css'` on this line - and ng-packagr inlined it at build time, so
241 KB of third-party CSS was compiled into this package's JavaScript (a 471 KB fesm bundle
carrying 522 .ck-editor rules) and, because a host registers the field type in its application
config, shipped in that host's initial bundle whether or not a rich-text field was ever opened. It
also pinned the CSS to whatever ckeditor5 version this package was built against, while the editor
itself comes from the host's own installed copy via `await import('ckeditor5')` at runtime.

The host now serves that file under the fixed bundle name `ckeditor5` and
CKEditorControlComponent.ngOnInit asks for it by name - see CKEDITOR5_STYLE, the
FlexFieldsStyleLoader it goes through, and this package's README for the single angular.json entry
a host adds. A host that forgets the entry gets one console error naming the file and quoting the
entry; the editor's DOM is still built, it just renders as blank/collapsed space until the
stylesheet arrives.

What remains here is this package's own theming on top of that CSS. It stays unscoped
(ViewEncapsulation.None on the component - CKEditor attaches balloons/dropdowns outside this
component's template subtree, where per-component style scoping would not reach them), and every
rule below had to survive the ordering reversal the move caused: ckeditor5.css used to be the
first thing in this stylesheet and therefore lost every equal-specificity tie to the rules under
it; it now arrives as a <link> appended to <head> after Angular has inserted these styles, so at
equal specificity it wins instead. Each !important below names the upstream declaration it exists
to outrank. */

/* CKEditor 5's balloon/dropdown panels (BalloonEditor's floating toolbar, link/table pickers, etc.)
attach to a wrapper appended directly under <body> and default to z-index 1000 (--ck-z-default: 1,
--ck-z-panel: default + 999). ng-bootstrap's modal window sits at 1055 - higher - so any of those
panels opened while a CKEditor is inside an <abp-modal> or similar ng-bootstrap dialog render fully
in-DOM and CSS-"visible", but painted underneath the modal, i.e. invisible to the user despite every
element reporting visible/opacity:1. Raising the base above any Bootstrap-derived dialog z-index
(Bootstrap's own scale tops out at 1080) fixes every derived panel at once; harmless when no modal
is present since there is nothing competing for the stacking order.

!important: ckeditor5.css sets `--ck-z-default: 1` on a bare `:root` of its own - the same selector
at the same specificity - and now arrives after this stylesheet, so without it the stock 1 wins and
every panel goes back to painting under the modal. The only other upstream declaration of this
token is `html.ck-fullscreen, body.ck-fullscreen`, which this !important would also outrank; that
rule belongs to CKEditor's Fullscreen plugin, which buildEditorConfig never registers, so it cannot
apply here - a fork that adds that plugin has to revisit this line. */
:root {
--ck-z-default: 1100;
--ck-z-default: 1100 !important;
}

/* CKEditor 5 ships no dark-mode palette: ckeditor5.css's own :root block hardcodes the four base
Expand Down Expand Up @@ -87,10 +109,9 @@
hover/active fill that is always a translucent tint of the current text color, correct in both
modes without a per-mode value of its own.

!important on every property: ckeditor5.css's own :root block is only injected once a CKEditor field
is first opened (its multi-megabyte payload is dynamic-imported - see
CKEditorControlComponent.ngOnInit), i.e. after this stylesheet - at equal :root specificity, source
order alone would otherwise let its stock light-mode palette win. Same reasoning as the
!important on every property: ckeditor5.css declares all six of these tokens on a bare `:root` of
its own - the same specificity as this block's `:root` half - and now arrives after this
stylesheet, so source order alone would let its stock light-mode palette win. Same reasoning as the
--ck-content-font-color override further down this file. */
:root,
body {
Expand All @@ -113,13 +134,37 @@ body {
sets a real border/background unconditionally, focused or not. Reusing those same custom properties
here (rather than hardcoding colors) keeps Basic and Full visually consistent and both track
whatever theme is active. Unscoped like the z-index override above, for the same reason
(ViewEncapsulation.None - this file is already global by design). */
(ViewEncapsulation.None - this file is already global by design).

No !important on the background: nothing upstream sets background or background-color on
.ck.ck-editor__editable_inline itself, so there is no equal-specificity tie for source order to
decide. The one higher-specificity background rule that reaches an editable,
".ck.ck-editor__main > .ck-editor__editable", is Full-mode-only and resolves to the very same
var(--ck-color-base-background) this line does - it won before the move and still wins, to no
visible difference. */
.ck.ck-editor__editable_inline {
background: var(--ck-color-base-background);
border-color: var(--ck-color-base-border);
}

/* ckeditor5-content.css hard-codes --ck-content-font-color to #000, entirely independent of
/* The border half of the rule above, deliberately split out and narrowed to the unfocused state.

It needs to outrank ckeditor5.css's own ".ck.ck-editor__editable_inline { border: 1px solid #0000 }"
- identical specificity, and the shorthand resets border-color, so after the move it would silently
win and put the invisible border back. It must NOT outrank
".ck.ck-editor__editable:not(.ck-editor__nested-editable).ck-focused { border: var(--ck-focus-ring) }",
which is how a focused editable (Basic and Full alike) gets its blue focus ring: a blanket
`border-color: … !important` at this specificity would beat that rule too and flatten the focus ring
to a plain grey border. `:not(.ck-focused)` is the right narrowing rather than a workaround - this
rule was always about the unfocused state, which is the one that reads as blank space - and it also
makes the declaration outrank the upstream shorthand on specificity alone, with !important left in
as the same belt-and-braces the tokens above use. The only other unfocused-editable border-color
upstream, ".ck.ck-editor__main > .ck-editor__editable:not(.ck-focused)", resolves to the same
var(--ck-color-base-border) as this line, so outranking it changes nothing visible. */
.ck.ck-editor__editable_inline:not(.ck-focused) {
border-color: var(--ck-color-base-border) !important;
}

/* ckeditor5.css hard-codes --ck-content-font-color to #000, entirely independent of
--ck-color-base-text above - asymmetric with .ck-content's own *background*, which has none of
its own and simply inherits whatever --ck-color-base-background resolves to (the rule above is
what makes that visible for Basic mode's editable; Full mode gets it unconditionally from
Expand All @@ -129,9 +174,13 @@ body {
Repointing the content token at the same base-text token closes that gap generically, off a token
this file already sets above - no further host-specific variable knowledge needed here beyond the
Bootstrap/LeptonX fallback chain the dark-theme block above already documents.
!important: ckeditor5-content.css loads later than this file - bundled with the dynamic
import('ckeditor5') in ngOnInit, not the static @import above - so at equal :root specificity,
source order would otherwise let its own #000 default win. Declared on the same `:root, body`

The .ck-content rules come from the same host-served ckeditor5.css as everything else: the dist
ships ckeditor5-editor.css and ckeditor5-content.css separately, but ckeditor5.css is the two
concatenated, and dist/ckeditor5.js imports no CSS at all - so nothing about .ck-content ever
arrived with the dynamic import('ckeditor5'), contrary to what this comment used to claim. That one
file, appended to <head> after Angular has inserted these styles, is what !important is for here:
at equal :root specificity its own #000 would otherwise win. Declared on the same `:root, body`
pair as the block above, for the same reason. */
:root,
body {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { FormGroup, Validators } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { RestService } from '@abp/ng.core';
import { LazyLoadService, RestService } from '@abp/ng.core';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { FlexFieldValue } from '@dignite/ng.flex-fields';
import { Observable, of } from 'rxjs';
import { DISABLE_FLEX_FIELDS_STYLE_LOADING_TOKEN, FlexFieldValue } from '@dignite/ng.flex-fields';
import type { Editor } from 'ckeditor5';
import { CKEDITOR5_STYLE } from './ckeditor-style';
import { CKEditorControlComponent } from './ckeditor-control.component';
import { CKEditorUploadAdapter } from './ckeditor-upload-adapter';

Expand Down Expand Up @@ -79,13 +81,29 @@ class OnPushHostComponent {
@Input() entity!: FormGroup;
}

/** Records what would have been appended to `<head>` - the same stub style-loader.service.spec.ts uses. */
class LazyLoadServiceStub {
readonly paths: string[] = [];

load(strategy: { path: string }): Observable<Event> {
this.paths.push(strategy.path);
return of(new CustomEvent('load'));
}
}

describe('CKEditorControlComponent', () => {
beforeEach(() => {
// @ngx-validate/core's validation directive attaches to any [formGroupName]/[formControlName]
// element and needs its blueprints token even though TestBed.createComponent() never runs CD here
// - view creation alone is enough to construct it.
//
// ckeditor5.css is the host application's to serve; no fixture has that bundle, and the load
// itself is exercised in the `style loading` block below, which opts back in.
TestBed.configureTestingModule({
providers: [{ provide: RestService, useValue: {} }],
providers: [
{ provide: RestService, useValue: {} },
{ provide: DISABLE_FLEX_FIELDS_STYLE_LOADING_TOKEN, useValue: true },
],
imports: [NgxValidateCoreModule.forRoot()],
});
});
Expand Down Expand Up @@ -216,4 +234,39 @@ describe('CKEditorControlComponent', () => {
// ckeditor-editor-config.spec.ts; nothing cheap to add here beyond re-asserting DOM presence, which
// the test above already does.
});

describe('style loading', () => {
let lazyLoadService: LazyLoadServiceStub;

// Configured after the outer beforeEach, so these providers come later in the testing module's
// provider list and win for both tokens: style loading goes back on, and the append is recorded
// instead of really reaching <head>.
beforeEach(() => {
lazyLoadService = new LazyLoadServiceStub();
TestBed.configureTestingModule({
providers: [
{ provide: LazyLoadService, useValue: lazyLoadService },
{ provide: DISABLE_FLEX_FIELDS_STYLE_LOADING_TOKEN, useValue: false },
],
});
});

it('asks the host for its ckeditor5.css bundle at init', () => {
const { fixture } = build(fieldValue());

fixture.detectChanges();

// The literal file name rather than `${CKEDITOR5_STYLE.bundleName}.css`: it is the contract with
// the host's angular.json entry, so a rename has to fail here instead of following the constant.
expect(lazyLoadService.paths).toEqual(['ckeditor5.css']);
});

it('describes the exact angular.json entry a host has to declare', () => {
// Quoted verbatim by this package's README and by the demo's angular.json.
expect(CKEDITOR5_STYLE).toEqual({
bundleName: 'ckeditor5',
input: 'node_modules/ckeditor5/dist/ckeditor5.css',
});
});
});
});
Loading