Creating a new component project (sc-chips) and loading it in a formbird template.
You can generate a new component project with the following steps. We will create an example component using the Angular Material Chips input at https://material.angular.io/components/chips/overview as we go:
- Make sure you’ve got Node.js 16 installed.
-
Install Angular CLI with: ``` npm install @angular/cli -g
3. Generate a project with Angular CLI inside the `formbird-sc-components` dir
ng generate application--prefix=sc --routing=false --style=scss Eg. for a component called sc-chips:
ng generate application sc-chips --prefix=sc --routing=false --style=scssThis will create an application project at `formbird-sc-components/projects/sc-chips`. A project entry is also added to the `formbird-sc-components/angular.json` file. This will contain the settings for building the project. The project is generated as an application because each component project is like a standalone application that registers the component as a custom element. 4. Generate an Angular module for the component with:
ng generate module--project= Eg. to generate a module called `ScChipsModule`:
ng generate module "sc-chips" --project=sc-chipsThe module is created at:
projects/sc-chips/src/app/sc-chips/sc-chips.module.ts`NOTE`: To make this work as an individual project that can be ported directly to the core application, you need to ensure the following: - `ScChipsModule` is added to the imports array in the `AppModule` at `projects/sc-chips/src/app/app.module.ts` - If it isn’t added yet by the angular CLI, add `BrowserModule` to the imports as well to avoid `null-injector` issues when using global services:
], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }5. Generate the component with the following:
ng generate component--project= --module= --viewEncapsulation=None --skipSelector=true --changeDetection=OnPush Eg.
ng generate component sc-chips --project=sc-chips --module=sc-chips --viewEncapsulation=None --skipSelector=true --changeDetection=OnPush`` The options used in the command are: -
viewEncapsulation- We use no viewEncapsulation so that css can reference the elements in the component. It’s possible to use viewEncapsulation to make the styles and selectors isolated to the component and in many cases this is preferable but it won’t work with libraries that have global css. -
skipSelector- Angular uses a selector to identify the component in the Angular html template, but we don’t need it when using the components as custom elements with Angular Elements -
changeDetection- we use
OnPush` change detection because it's faster than other Angular change detection methods.The following component files will be created from this action:
projects/sc-chips/src/app/sc-chips/sc-chips.component.ts projects/sc-chips/src/app/sc-chips/sc-chips.component.html projects/sc-chips/src/app/sc-chips/sc-chips.component.scss projects/sc-chips/src/app/sc-chips/sc-chips.component.spec.ts
The component is automatically injected into thesc-chips
module (as an entry in declarations) due to the--module
switch.@NgModule({ declarations: [ScChipsComponent], imports: [ CommonModule ] }) export class ScChipsModule { }
What you now have is a complete set of files that will make up our component element. Thesc-chips.component.ts
file is where you put in most of the internal logic to operate the view in thesc-chips.component.html
file. Thesc-chips.component.scss
file is where you add scss styles that can be directly applied to the elements inside the component.html file.The
sc-chips.component.html
will by default have the following contents:<p>sc-chips works!</p>
Therefore, we have a perfectly working component that we can use in a formbird template. We’ll use this for now and continue into the process of displaying it in the formbird core application. We will continue updating the component to its final appearance and function in the sectionCompleting the sc-chips Component
6. In order to register the component as a custom element, we need to do the following in our module: 1. ImportCustomElementService
in the imports section of the module ts file at projects/sc-chips/src/app/sc-chips/sc-chips.module.ts. All static imports must be before any non-import code, as described at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import:import {CustomElementService} from 'formbird-sc-shared';
2. ImportCUSTOM_ELEMENTS_SCHEMA
and Injector from@angular/core
by adding them to the existing import from@angular/core
:import { CUSTOM_ELEMENTS_SCHEMA, Injector, NgModule } from '@angular/core';
3. Add theCUSTOM_ELEMENTS_SCHEMA
under@NgModule
. 4. AddScChipsComponent
as an entry into the declarations array (if not already added).Add the component into the bootstrap array. This will allow the `ScChipsComponent` to be bootstrapped before we use it in the `ScChipsModule` constructor below at step e. If you fail to do this, angular will fail to recognise our component as an angular component. ``` @NgModule({ declarations: [ScChipsComponent], bootstrap: [ScChipsComponent], schemas: [ CUSTOM_ELEMENTS_SCHEMA ], imports: [ CommonModule ], providers: [ // add your services here ] }) export class ScChipsModule { } ```
- Add a constructor to the module class in projects/sc-chips/src/app/sc-chips/sc-chips.module.ts. This should contain the code for registering the component as a custom element under the name that will be used in the componentName field of the component definition in the formbird template. If you want multiple names for the component, you can add calls
convertToCustomElement()
using a different name:// inside ScChipsModule {} export class ScChipsModule { constructor( private injector: Injector, private customElementService: CustomElementService ) { customElementService.convertToCustomElement( ScChipsComponent, 'sc-chips', this.injector ); }
- This step involves setting up the project’s build settings. A concrete example of the changes in
angular.json
is provided below before this section ends.
Webpack’s Externals Configuration Option
By default, angular builds our applications using webpack. There is a webpack configuration option called externals (https://webpack.js.org/configuration/externals/) that allows the re-use of common libraries found in the core and in other component projects. This drastically lowers the bundle size of our projects and improves runtime performance. One of the things that is often overlooked about a large bundle size is that the performance impact comes from the Javascript parse time after downloading of the libraries rather than the downloading of the libraries themselves, so keeping the bundle size down improves the performance both by reducing the download time and the Javascript parse time.
We need to configure our
sc-chips
project’s settings in theangular.json
file in the root of the component project. Make sure that the project uses a builder that allows it to extend its default webpack build configuration. Locate the project’s builder option under build and replace@angular-devkit/build-angular:browser
with@angular-builders/custom-webpack:browser
:"formbird-sc-components": { "projectType": "application", "schematics": {}, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-builders/custom-webpack:browser",
This needs to be changed in the builder entry for any project that brings multiple components together and an individual project for a single component, such as an sc-chips project. Builder entries under sections like serve don’t need to be changed.Now that we have set the project to use the ‘custom-webpack’ builder, we will specify how this builder behaves in the following option:
angular.json>projects>sc-chips>architect>build>options>customWebpackConfig
Copy this additional build option:"options": { "customWebpackConfig": { "path": "projects/sc-chips/extra-webpack.config.js", "mergeStrategies": { "externals" : "replace" } }
Copy this file from the core app repository to ensure we have the latest version:fieldtec-web/extra-webpack.config.js
. Paste it into the path we specified incustomWebpackConfig
."path": "projects/sc-chips/extra-webpack.config.js"
This file has declarations that specify what libraries are shared by the core and other component projects. We intend to utilize most of these libraries inside our project.Minor updates on extra-webpack.config.js
Add an
output.jsonpFunction
entry for safe usage of multiple webpack runtimes on the same webpage. The project name is normally appended towebpackJsonp
to serve as the value for this option:module.exports = { output: { jsonpFunction: "webpackJsonpScChips" }, externals: externals };
See details on https://webpack.js.org/configuration/output/#outputjsonpfunctionOn Using Angular-Material Libraries
The angular-material core library is already shared by the core, hence you see the entry at
extra-webpack.config.js
:"@angular/material/core": "ng.material.core",
Due to this setup, we need a bit of housekeeping when using ng-material libraries. If we’re about to use an angular-material library that isn’t yet shared (declared inextra-webpack.config.js
) we need to share it ourselves.In this project, we want to use
@angular/material/chips
. If extra-webpack.config.js doesn’t have an entry for@angular/material/chips
in it, then it means that the core doesn’t use this library and we need to share it ourselves with the following procedure: 1. Locate the library’s UMD file. The UMD file installs the library into the global browser object; this is the manner in which the libraries are shared across bundles. For our example, the chips UMD is atnode_modules/@angular/material/bundles/material-chips.umd.js
2. Add the UMD file’s path into the scripts section under the project’s build settings atangular.json
. This will make sure the UMD is loaded once your project is loaded. This bundles the UMD file insidescripts.js
which is going to be inserted in the project’s finalindex.html
. See the code below at CSS Packaging Settings for the actual example. 3. Determine the library’s global reference once installed into the global browser object. You may need to read into the UMD file to do this. Look into the first few lines of the UMD and look for something like this:global.ng.material.chips = {}
In this example, we can determine that the library’s global reference isng.material.chips
- Declare/add the library as a shared library in
extra-webpack.config.js
:// material libs externalised by this project "@angular/material/chips": "ng.material.chips"
You will need to do this for every angular-material library you will use that isn’t yet shared or declared in extra-webpack.config.js
CSS Packaging Settings
Add the
extractCss
option inangular.json
to build the styles to a separate css file in a build.By default the Angular CLI outputs the styles to JS in a development build but CSS in a production environment for faster builds, but it means the paths in the component document need to be different in a production and development build, so it’s better if we output to CSS in a development build as well as production.
outputHashing and vendorChunk options
For simplicity, we’ll set the
outputHashing
property set tonone
. This will prevent suffixing the output files (built) with a hash number so as to identify between different versions of built files.With output hashing:
main.6343393ad336d2e5b2b59.js polyfills.a1199da94ed4671acb7b.js runtime.c51vd5b1c616d9ffddc1.js styles.09e2c710755c8867a460.css
Additionally, we’ll set the
vendorChunk
property to true. This means that imported libraries(except those declared in extra-webpack.config.js)
will be bundled into a separate file calledvendor.js
instead of getting bundled into the main file. This allows a more detailed picture on the size of your bundles.The following files are built without output hashing. No UUID suffix was added to the filename. Also note that no
vendor.js
file was generated because we didn’t use a third party library that isn’t declared as external (it means all the 3rd parties we need are already loaded by fieldtec-web via the externals option). If we did use an external library we’d see avendor.js
file here.main.js polyfills.js runtime.js styles.css
"root": "projects/sc-chips", "sourceRoot": "projects/sc-chips/src", "prefix": "sc", "architect": { "build": { "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { "path": "projects/sc-chips/extra-webpack.config.js", "mergeStrategies": { "externals" : "replace" } }, "extractCss": true, "outputHashing": "none", "vendorChunk": true, "outputPath": "dist/sc-chips", ... ... "scripts": [ ... "node_modules/@angular/material/bundles/material-chips.umd.js" ]
8. Inprojects/sc-chips/src/app/app.module.ts
, we need to make it so that the component is not bootstrapped as an Angular application because it’s run as a custom element, so we need to replace the following:bootstrap: [AppComponent] // remove or comment-out }) export class AppModule { }
With:// do not bootstrap the app component when exporting custom element // because an Angular app doesn't run. An ngDoBootstrap // function is defined that does nothing. // bootstrap: [AppComponent] }) export class AppModule { constructor() {} // ngDoBootstrap needs to do nothing when exporting custom elements // that the normal Angular app bootstrap doesn't take place ngDoBootstrap() {} }
9. Remove theenableProdMode
line fromprojects/sc-chips/src/main.ts
because this mode should only be enabled by the core app. The custom element is using the Angular platform that is already loaded so prod mode should not be enabled again. You’ll get an error if you omit this because it will install another instance of the angular platform (on top of what’s already been installed by the core) in production mode:import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); // remove }
If you omit to do this, the component will not load and you’ll get the following error in the browser console when running in production mode:Uncaught Error: Cannot enable prod mode after platform setup.
10. Build the component project with: - Build the required formbird libraries (required only once)yarn build:formbird
- you can also use this to build the formbird librariesng build formbird-services && ng build formbird-sc-shared && ng build formbird-mapping
- Actual sc-chips build:`Note`: you can also set the source map option at `angular.json` so you will be able to debug the component in the browser debugger ``` ng build sc-chips --source-map ```
The built project is
formbird-sc-components/dist/sc-chips
. - Add a constructor to the module class in projects/sc-chips/src/app/sc-chips/sc-chips.module.ts. This should contain the code for registering the component as a custom element under the name that will be used in the componentName field of the component definition in the formbird template. If you want multiple names for the component, you can add calls
-
Create a component document for the
sc-chips
component. Supply itsvendorLibrariesRel
with entries that lead to built project’s files.You can find the entries that go in vendorLibrariesRel by looking at
formbird-sc-components/dist/sc-chips/index.html
:<script src="runtime.js" defer></script> <script src="polyfills.js" defer></script> <script src="styles.js" defer></script> <script src="scripts.js" defer></script> <script src="vendor.js" defer></script> <script src="main.js" defer></script>
You’ll need to add the
styles.css
file and each script in theindex.html
as an entry invendorLibrariesRel
. They’ll need to be added in the the same order they appear inindex.html
:``` "vendorLibrariesRel": [ { "documentId": "ef1dbb2d-2c0e-4f1c-a42d-77c1562be6b2", "name": "scChipsProjectStyles", "fileName": "vendor/custom-component-modules/sc-chips/styles.css" }, { "documentId": "58d8730c-3ff6-4856-9401-0c15f42f4455", "name": "scChipsProjectRunTime", "fileName": "vendor/custom-component-modules/sc-chips/runtime.js" }, { "documentId": "abaaad2b-90fd-4b7b-b767-792b85dfb93f", "name": "scChipsProjectPolyfills", "fileName": "vendor/custom-component-modules/sc-chips/polyfills.js" }, { "documentId": "ef1dbb2d-2c0e-4f1c-a42d-77c1562be6b2", "name": "scChipsProjectScripts", "fileName": "vendor/custom-component-modules/sc-chips/scripts.js" }, { "documentId": "ef1dbb2d-2c0e-4f1c-a42d-77c1562be6b2", "name": "scChipsProjectVendor", "fileName": "vendor/custom-component-modules/sc-chips/vendor.js" }, { "documentId": "7b431aa0-0173-4cdc-ba74-c6c729f03e91", "name": "scChipsProjectMain", "fileName": "vendor/custom-component-modules/sc-chips/main.js" } ],
```
If
scripts.js
orvendor.js
isn’t present in the build output, there’s no need for you to add it into thevendorLibsRel
.If you’ve added UMD files into the scripts section under the project’s build settings, as instructed in the subsection On Using Angular-Material Libraries, then a
scripts.js
file will be generated. If you’ve referenced third party dependencies, then avendor.js
will be generated. -
Create a test template for sc-chips with the following in the components array:
"components": [ { "componentName": "sc-chips", "name": "myTags", "label": "Test sc-chips", "scChipsPlaceholder": "custom", "defaultValue": [ "Formbird", "demo", "component", "sc-chips" ] } ]
Or download the template JSON file atsc-chips
template.json -
Copy the folder containing the built files from
formbird-sc-components/dist/sc-chips
tofieldtec-web/server/public/vendor/custom-component-modules
- Make sure you have started formbird with the following in the fieldtec-web dir:
npm start
- Load the test template created in step
12
on port4200
, withhttp://localhost:4200/form/65068020-d221-11ea-a639-896deb7486cc