Skip to content

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:

  1. Make sure you’ve got Node.js 16 installed.
  2. 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=scss This 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-chips The 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 useOnPush` 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 the sc-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. The sc-chips.component.ts file is where you put in most of the internal logic to operate the view in the sc-chips.component.html file. The sc-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 section Completing 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. Import CustomElementService 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. Import CUSTOM_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 the CUSTOM_ELEMENTS_SCHEMA under @NgModule. 4. Add ScChipsComponent 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 { }
    ```
    
    1. 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 ); }
    2. 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 the angular.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 in customWebpackConfig. "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 to webpackJsonp to serve as the value for this option: module.exports = { output: { jsonpFunction: "webpackJsonpScChips" }, externals: externals }; See details on https://webpack.js.org/configuration/output/#outputjsonpfunction

    On 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 in extra-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 at node_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 at angular.json. This will make sure the UMD is loaded once your project is loaded. This bundles the UMD file inside scripts.js which is going to be inserted in the project’s final index.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 is ng.material.chips

    1. 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 in angular.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 to none. 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 called vendor.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 a vendor.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. In projects/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 the enableProdMode 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 libraries

    ng 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.

  3. Create a component document for the sc-chips component. Supply its vendorLibrariesRel 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 the index.html as an entry in vendorLibrariesRel. They’ll need to be added in the the same order they appear in index.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 or vendor.js isn’t present in the build output, there’s no need for you to add it into the vendorLibsRel.

    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 a vendor.js will be generated.

  4. 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 at sc-chips template.json

  5. Copy the folder containing the built files from formbird-sc-components/dist/sc-chips to fieldtec-web/server/public/vendor/custom-component-modules

  6. Make sure you have started formbird with the following in the fieldtec-web dir: npm start
  7. Load the test template created in step 12 on port 4200, with http://localhost:4200/form/65068020-d221-11ea-a639-896deb7486cc