This project is a Google Docs clone built with Angular for the frontend, Angular Material Design for the UI components, Quill.js for the rich text editor, and a backend API developed using ASP.NET Core with the repository pattern. The API is documented using Swagger OpenAPI documentation, and Entity Framework Core is used for database access with SQLite as the underlying database engine.
Before running the application, make sure you have the following installed:
git clone https://github.com/alameenboss/google-docs-clone.git
frontend
directory:cd google-docs-clone/frontend
npm install
cd ../backend
dotnet restore
dotnet ef database update
cd google-docs-clone/frontend
ng serve
Access the frontend application at http://localhost:4200.
cd google-docs-clone/backend
dotnet run
The API server will be available at http://localhost:5000.
Contributions are welcome! If you'd like to contribute to this project, please follow these steps:
This project is licensed under the MIT License.
ngx-quill is an angular (>=2) module for the Quill Rich Text Editor containing all components you need.
If you like my work, feel free to support it. Donations to the project are always welcomed :)
PayPal: PayPal.Me/bengtler
quill-view
and quill-view-html
componentAngularngx-quillsupportedv16>= 22.0.0until Dec, 2024v15>= 20.0.0 <= 22.0.0until May, 2024v14>= 17.0.0 < 20until Dec, 2023
npm install ngx-quill
npm install ngx-quill@1.6.0
@angular/core
, @angular/common
, @angular/forms
, @angular/platform-browser
, quill
v1.x, @types/quill
v1.x and rxjs
- peer dependencies of ngx-quillnode_modules/quill/dist
), or add them in your css/scss files with @import
statements, or add them external stylings in your build process.@import '~quill/dist/quill.bubble.css'; // or @import '~quill/dist/quill.snow.css';
QuillModule
from ngx-quill
:import { QuillModule } from 'ngx-quill'
QuillModule
to the imports of your NgModule:@NgModule({ imports: [ ..., QuillModule.forRoot() ], ... }) class YourModule { ... }
<quill-editor></quill-editor>
in your templates to add a default quill editorHINT: If you are using lazy loading modules, you have to add QuillModule.forRoot()
to your imports in your root module to make sure the Config
services is registered.
Nothing to do here :)
It's possible to set custom default modules and Quill config options with the import of the QuillConfigModule
from the ngx-quill/config
. This module provides a global config, but eliminates the need to import the ngx-quill
library into the vendor bundle:
import { QuillConfigModule } from 'ngx-quill/config'; @NgModule({ imports: [ ..., QuillConfigModule.forRoot({ modules: { syntax: true, toolbar: [...] } }) ], ... }) class AppModule {}
Registering the global configuration can be also done using the standalone function if you are bootstrapping an Angular application using standalone features:
import { provideQuillConfig } from 'ngx-quill/config'; bootstrapApplication(AppComponent, { providers: [ provideQuillConfig({ modules: { syntax: true, toolbar: [...] } }) ] })
If you want to use the syntax
module follow the Syntax Highlight Module Guide.
See Quill Configuration for a full list of config options.
The QuillModule
exports the defaultModules
if you want to extend them :).
Per default when Quill.register
is called and you are overwriting an already existing module, QuillJS logs a warning. If you pass customOptions
or customModules
ngx-quill is registering those modules/options/formats for you.
In e.g. an angular univeral project your AppModule
and so QuillModule.forRoot()
is executed twice (1x server side, 1x browser). QuillJS is running in a mocked env on server side, so it is intendet that every register runs twice.
To subpress those expected warnings you can turn them off by passing suppressGlobalRegisterWarning: true
.
Ngx-quill updates the ngModel or formControl for every user
change in the editor. Checkout the QuillJS Source parameter of the text-change
event.
If you are using the editor reference to directly manipulate the editor content and want to update the model, pass 'user'
as the source parameter to the QuillJS api methods.
html
, values: html | object | text | json
, sets the model value type - html = html string, object = quill operation object, json = quill operation json, text = plain textconst modules = { toolbar: [ ['bold', 'italic', 'underline', 'strike'], // toggled buttons ['blockquote', 'code-block'], [{ 'header': 1 }, { 'header': 2 }], // custom button values [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent [{ 'direction': 'rtl' }], // text direction [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown [{ 'header': [1, 2, 3, 4, 5, 6, false] }], [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme [{ 'font': [] }], [{ 'align': [] }], ['clean'], // remove formatting button ['link', 'image', 'video'] // link and image, video ] };
snow
false
, boolean (only for format="html")[styles]="{height: '250px'}"
Insert text here ...
document.body
, pass 'self' to attach the editor elementinvalid
and add ng-invalid
classinvalid
and add ng-invalid
class, only set invalid if editor text not empty --> if you want to check if text is required --> use the required attributefalse
[required]="true"
- default: false, boolean expected (no strings!)// typings.d.ts declare module '!!raw-loader!*.css' { const css: string; export default css; } // my.component.ts const quillCSS$ = defer(() => import('!!raw-loader!quill/dist/quill.core.css').then((m) => { const style = document.createElement('style'); style.innerHTML = m.default; document.head.appendChild(style); }) ).pipe(shareReplay({ bufferSize: 1, refCount: true })); @Component({ template: '<quill-editor [beforeRender]="beforeRender"></quill-editor>', }) export class MyComponent { beforeRender = () => firstValueFrom(quillCSS$); }
{ import: string; whitelist: any[] }
--> this overwrites this options globally !!!// Example with registering custom fonts customOptions: [{ import: 'formats/font', whitelist: ['mirza', 'roboto', 'aref', 'serif', 'sansserif', 'monospace'] }]
{ implementation: any; path: string }
--> this overwrites this modules globally !!!// The `implementation` may be a custom module constructor or an Observable that resolves to // a custom module constructor (in case you'd want to load your custom module lazily). // For instance, these options are applicable: // import BlotFormatter from 'quill-blot-formatter'; customModules = [ { path: 'modules/blotFormatter', implementation: BlotFormatter } ] // Or: const BlotFormatter$ = defer(() => import('quill-blot-formatter').then(m => m.default)) customModules = [ { path: 'modules/blotFormatter', implementation: BlotFormatter$ } ]
customOptions
and customModules
Demo Repo[quill-editor-toolbar]
:Try to not use much angular magic here, like (output)
listeners. Use native EventListeners
<quill-editor> <div quill-editor-toolbar> <span class="ql-formats"> <button class="ql-bold" [title]="'Bold'"></button> </span> <span class="ql-formats"> <select class="ql-align" [title]="'Aligment'"> <option selected></option> <option value="center"></option> <option value="right"></option> <option value="justify"></option> </select> <select class="ql-align" [title]="'Aligment2'"> <option selected></option> <option value="center"></option> <option value="right"></option> <option value="justify"></option> </select> </span> </div> </quill-editor>
top
, possible values top
, bottom
warn
, error
, log
or false
to deactivate logging, default: warn
user
(quill source user) or all
change should be trigger model update, default user
. Using all
is not recommended, it cause some unexpected sideeffects.onContentChanged
, onEditorChanged
, ngModel
and form control value changes. Improves performance (especially when working with large, >2-3 MiB Deltas), as neither editorChangeHandler
, nor textChangeHandler
handler runs internally.null
, but you can set it e.g. to empty stringeditor // Quill
{ editor: editorInstance, // Quill html: html, // html string text: text, // plain text string content: content, // Content - operatins representation delta: delta, // Delta oldDelta: oldDelta, // Delta source: source // ('user', 'api', 'silent' , undefined) }
{ editor: editorInstance, // Quill range: range, // Range oldRange: oldRange, // Range source: source // ('user', 'api', 'silent' , undefined) }
{ editor: editorInstance, // Quill event: 'text-change' // event type html: html, // html string text: text, // plain text string content: content, // Content - operatins representation delta: delta, // Delta oldDelta: oldDelta, // Delta source: source // ('user', 'api', 'silent' , undefined) }
or
{ editor: editorInstance, // Quill event: 'selection-change' // event type range: range, // Range oldRange: oldRange, // Range source: source // ('user', 'api', 'silent' , undefined) }
{ editor: editorInstance, // Quill source: source // ('user', 'api', 'silent' , undefined) }
{ editor: editorInstance, // Quill source: source // ('user', 'api', 'silent' , undefined) }
In most cases a wysiwyg editor is used in backoffice to store the content to the database. On the other side this value should be used, to show the content to the enduser.
In most cases the html
format is used, but it is not recommended by QuillJS, because it has the intention to be a solid, easy to maintain editor. Because of that it uses blots and object representations of the content and operation.
This content object is easy to store and to maintain, because there is no html syntax parsing necessary. So you even switching to another editor is very easy when you can work with that.
ngx-quill
provides some helper components, to present quilljs content.
In general QuillJS recommends to use a QuillJS instance to present your content. Just create a quill editor without a toolbar and in readonly mode. With some simple css lines you can remove the default border around the content.
As a helper ngx-quill
provides a component where you can pass many options of the quill-editor
like modules, format, formats, customOptions, but renders only the content as readonly and without a toolbar. Import is the content
input, where you can pass the editor content you want to present.
html
, values: html | object | text | json
, sets the model value type - html = html string, object = quill operation object, json = quill operation json, text = plain textsnow
warn
, error
, log
or false
to deactivate logging, default: warn
false
, boolean (only for format="html")<quill-view [content]="content" format="text" theme="snow"></quill-view>
Most of you will use the html
format (even it is not recommended). To render custom html with angular you should use the [innerHTML]
attribute.
But there are some pitfalls:
div
-tag that has the innerHTML
attribute and add the ql-editor
class. Wrap your div in another div
-tag with css classes ql-container
and your theme, e.g. ql-snow
.:<div class="ql-container ql-snow" style="border-width: 0;"> <div class="ql-editor" [innerHTML]="byPassedHTMLString"> </div> </div>
After that your content should look like what you expected.
If you store html in your database, checkout your backend code, sometimes backends are stripping unwanted tags as well ;).
As a helper ngx-quill
provides a component where you can simply pass your html string and the component does everything for you to render it:
<quill-view-html [content]="htmlstring" theme="snow"></quill-view-html>
snow
false
, boolean (uses DomSanitizer to bypass angular html sanitation when set to false)Angular templates provide some assurance against XSS in the form of client side sanitizing of all inputs https://angular.io/guide/security#xss.
Ngx-quill components provide the input paramter sanitize
to sanitize html-strings passed as ngModel
or formControl
to the component.
It is deactivated per default to avoid stripping content or styling, which is not expected.
But it is recommended to activate this option, if you are working with html strings as model values.
The var()
function is used to insert the value of a CSS variable.
CSS variables have access to the DOM, which means that you can create variables with local or global scope, change the variables with JavaScript, and change the variables based on media queries.
A good way to use CSS variables is when it comes to the colors of your design. Instead of copy and paste the same colors over and over again, you can place them in variables.
The following example shows the traditional way of defining some colors in a style sheet (by defining the colors to use, for each specific element):
body { background-color: #1e90ff; }
h2 { border-bottom: 2px solid #1e90ff; }
.container {
color: #1e90ff;
background-color: #ffffff;
padding: 15px;
}
button {
background-color: #ffffff;
color: #1e90ff;
border: 1px solid #1e90ff;
padding: 5px;
}
The var()
function is used to insert the value of a CSS variable.
The syntax of the var()
function is as follows:
var(--name, value)
ValueDescriptionnameRequired. The variable name (must start with two dashes)valueOptional. The fallback value (used if the variable is not found)
Note: The variable name must begin with two dashes (--) and it is case sensitive!
ADVERTISEMENT
First of all: CSS variables can have a global or local scope.
Global variables can be accessed/used through the entire document, while local variables can be used only inside the selector where it is declared.
To create a variable with global scope, declare it inside the :root
selector. The :root
selector matches the document's root element.
To create a variable with local scope, declare it inside the selector that is going to use it.
The following example is equal to the example above, but here we use the var()
function.
First, we declare two global variables (--blue and --white). Then, we use the var()
function to insert the value of the variables later in the style sheet:
:root {
--blue: #1e90ff;
--white: #ffffff;
}
body { background-color: var(--blue); }
h2 { border-bottom: 2px solid var(--blue); }
.container {
color: var(--blue);
background-color: var(--white);
padding: 15px;
}
button {
background-color: var(--white);
color: var(--blue);
border: 1px solid var(--blue);
padding: 5px;
}
Advantages of using var() are:
To change the blue and white color to a softer blue and white, you just need to change the two variable values:
:root {
--blue: #6495ed;
--white: #faf0e6;
}
body { background-color: var(--blue); }
h2 { border-bottom: 2px solid var(--blue); }
.container {
color: var(--blue);
background-color: var(--white);
padding: 15px;
}
button {
background-color: var(--white);
color: var(--blue);
border: 1px solid var(--blue);
padding: 5px;
}
The numbers in the table specify the first browser version that fully supports the var()
function.
Functionvar()49.015.031.09.136.0