How to pass a parameter from html page to angular custom element - javascript

I've created an angular custom element from an angular component that I want call from a normal html page.
The component requires a parameter, which works when it's called as a component from within an angular project, but I cannot figure out how to pass the parameter from the html tag to the custom element.
Current I'm trying to use:
#Input() crs: string
in the component and:
<station-info crs="btn"></station-info>
in the html tag, but the crs never makes it into the component.
What I'd like to know is the correct way to pass the parameter from the html tag to the component after it has been converted to a custom element?

I eventually worked it out, so here's my write up of how to do it:
How To Create And Use A Custom Element
Install angular elements
ng add #angular/elements
npm i --save-dev concat fs-extra
In tsconfig.json, ensure target uses es2015 or newer:
"target": "es2015",
Add the following to app.module.ts
==> Add the imports
import { Injector} from '#angular/core';
import { createCustomElement } from '#angular/elements';
==> Add the component to the bootstrap array
bootstrap: [AppComponent]
==> Change the constructor to the following:
constructor(
private injector: Injector
) {
const el = createCustomElement(AppComponent, { injector });
customElements.define('station-info', el); // <-- 'station-info' is the name of the html tag you want to use
}
ngDoBootstrap() {}
To enable parameters to be passed from the HTML tag to the custom element, do the following steps
In app.component.ts:
Accept any input parameters using #Input and change any parameters retrieved from ActivatedRoute to use the #Input variables
Note: To ensure the input variables on the HTML tag are bound to the #Input properties, you must pass the tag attribute name to the #Input method i.e. #Input('crs') (see example below)
e.g.
Delete these:
this.crs = this.activatedRouteService.snapshot.params.crs.toLowerCase();
this.stationName = this.activatedRouteService.snapshot.params.stationName.toLowerCase();
Add these (insert after export class):
#Input('crs') crs: string
#Input('stationName') stationName: string;
In ngOnInit(), navigate to the component, passing on any input parameters:
e.g.
this.router.navigateByUrl('/station-details/' + this.crs + '/' + this.stationName);
In app-routing.module.ts
Delete (so there is no automatic routing):
{ path: '', redirectTo: 'station-details', pathMatch: 'full' },
Pass any parameters on the appropriate path:
{ path: 'station-details/:crs/:stationName', component: StationDetailsComponent,
In the custom HTML tag
Add the parameters as follows:
<station-info crs="btn" station-name="Brighton" client="tl" has-template="true"></station-info>
Create a build-component.js file in the root folder with the following content
Note: dist/dev is the folder structure automatically created by the compiler
const fs = require('fs-extra');
const concat = require('concat');
build = async () =>{
const files = [
'./dist/dev/runtime.js',
'./dist/dev/polyfills.js',
'./dist/dev/scripts.js',
'./dist/dev/main.js'
];
await fs.ensureDir('station-info'); // <-- Use the same name here as in customElements.define
await concat(files, 'station-info/station-info.js'); // <-- this is where you want to save the resulting files
}
build();
Compile and concatenate the resulting files
==> Add build-component.js script to package.json
"scripts": {
"build:component": "ng build --configuration production --output-hashing none && node build-component.js",
}
==> Run the script
npm run build:component
To use the custom element, add the following to an html page
Note: You also have to ensure any external assets that the custom element requires are present e.g. images and files
If you want to pass a parameter:

You can use property binding to pass data from parent to child.
Like so,
Use property binding to bind the item property in the child to the currentItem property of the parent.
<app-item-detail [item]="currentItem"></app-item-detail>
In the parent component class, designate a value for currentItem:
export class AppComponent {
currentItem = 'Television';
}
Link to angular docs:
https://angular.io/guide/inputs-outputs

Related

How to access js file in Angular

I've got a component where I'm trying to define a function that reads through the following js file and checks if a certain string value is contained in it. How can I do it? It's path is '../../../assets/beacons.js' (from my component) and it's named beacons.js
const allBeacons = {
"RIPE": [
"84.205.64.0/24",
"84.205.65.0/24",
"84.205.67.0/24",
"84.205.68.0/24",
"84.205.69.0/24",
"84.205.70.0/24",
"84.205.71.0/24",
"84.205.74.0/24",
"84.205.75.0/24",
"84.205.76.0/24",
"84.205.77.0/24",
"84.205.78.0/24",
"84.205.79.0/24",
"84.205.73.0/24",
"84.205.82.0/24",
"93.175.149.0/24",
"93.175.151.0/24",
"93.175.153.0/24"].map(i => i.toLowerCase()),
"rfd.rg.net": [
"45.132.188.0/24",
"45.132.189.0/24",
"45.132.190.0/24",
"45.132.191.0/24",
"147.28.32.0/24",
"147.28.33.0/24",
"147.28.34.0/24",
"147.28.35.0/24",
"147.28.36.0/24",
"147.28.37.0/24",
"147.28.38.0/24",
"147.28.39.0/24",
"147.28.40.0/24",
"147.28.41.0/24",
"147.28.42.0/24",
"147.28.43.0/24",
"147.28.44.0/24",
"147.28.45.0/24",
"147.28.46.0/24",
"147.28.47.0/24"].map(i => i.toLowerCase())
}
You need to include the js file in the bundle by telling angular about it:
Open angular.json, and add the path to the "scripts" array:
"scripts": ["src/assets/beacons.js"]
In the top of your component file (before class declaration) declare the variable as global so typescript won't complain about it (the name must match to the one in the js file):
type Beacons = {/* Define type if you want */} | any
declare const allBeacons: Beacons
Now you can use it as a global variable in your app:
ngOnInit() {
console.log(allBeacons)
}

Use of jQuery code in Angular app.component.ts

I am working on Angular app. I imported jQuery package using following code.
$ npm install jquery --save
I am trying to transfer my .js data of html web page to app.component.ts of angular. My setup for JQuery is done in angular app and I even tested it.
I am having checkbox in my web page, which will be selected to remove file using button click. To achieve this, I used the following code in JS.
function RemoveFile() {
var files = document.getElementById("files").children;
for (var i = 1; i < files.length; i++) {
if (files[i].children[0].children[0].checked) {
files[i].remove();
i--;
}
}
}
It is working fine in normal html page but when I used it in app.component.ts. It is giving me the following
Property 'checked' does not exist on type 'Element'
How can I rectify it or used something like this?
selectedFile.children[4].children[0].checked = $(this)[0].checked;
jQuery should only be used in Angular in the most exceptional of cases. Angular is a javascript framework for managing the DOM dynamically based on your model. If you manage the DOM outside of Angular you will more than likely suffer unexpected results.
In your case, I'm going to assume you've got some array of files in your component.
component.ts
export class MyComponent {
files: any[] = [];
ngOnInit() {
// somehow get the files
this.files = [];
}
}
And I'm going to assume that in your HTML you've built some kind of list containing a nested checkbox for each file.
The user will check some checkboxes, and then press a button to remove those files from the list (and probably perform some other action).
component.html
<div id="files">
<div *ngFor="let file of files">
<input type="checkbox" />
</div>
</div>
<button (click)="RemoveFile()">Remove files</button>
So you have used Angular to build the list, but now you want to use jQuery to "unbuild" the list? Why would you do that?
The power of Angular is in its simplicity - the HTML will reflect whatever's in your files array. So if you want to "unbuild" your list, then simply remove items from the array.
Admittedly, it's a little trickier when you have to consider things like an array of checkboxes, but that's no excuse to resort to using jQuery.
Since you are effectively going to build a form with a dynamic array, the best way to do handle this would be to use a reactive form with a form array.
Import the reactive forms module in your app module:
app.module.ts
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
#NgModule({
imports: [
FormsModule,
ReactiveFormsModule
]
}
In your component you will need to inject FormBuilder from #angular/forms. Then build a form once you have your model (which might be inside a service subscription).
You will then delete the files in the form's submit handler. You do have to splice two arrays, but the form array is built from the files array, so they have matching indexes.
component.ts
import { FormBuilder, FormGroup, FormArray } from '#angular/forms';
// ... other imports
export class MyComponent {
constructor(private formBuilder: FormBuilder) {}
files: any[] = [];
form: FormGroup;
private filesFormArray: FormArray;
ngOnInit(): void {
// somehow get the files
this.files = [];
// Build an array of form groups if you want each file to have multiple controls.
// Build an array of form controls if you want each file to have a single control
// I will just add one control per file - an unchecked checkbox,
// but use a form group per file to show how it is done
const filesFormGroups = this.files.map(x => this.formBuilder.group({
delete: this.formBuilder.control(false)
}));
// Build the form array. Store a reference to it for easy access later
this.filesFormArray = this.formBuilder.array(filesFormGroups);
// Build the whole form. At the moment there will only be files.
this.form = this.formBuilder.group({
files: this.filesFormArray
});
}
onSubmit(): void {
// start at the end of the array and work backwards
for (let i = this.files.length - 1; i >= 0; i--) {
// get the checkbox from the form array
const checkBox = this.filesFormArray.controls[i].get('delete') as FormControl;
if (checkbox.value) {
// remove the item from the model and the form
this.files.splice(i, 1);
this.filesFormArray.controls.splice(i, 1);
}
}
}
}
In your HTML, you still build from your files, but also bind to the form. You use the directives formGroupName, formArrayName, and formControlName to match the structure of the form group you built in your component.
component.html
<form [formGroup]="form" (submit)="onSubmit()">
<div formArrayName="files">
<div *ngFor="let file of files; let i = index" [formGroupName]="i">
<input type="checkbox" formControlName="delete" />
</div>
</div>
<button>Remove files</button>
</form>
// First install jQuery
npm install --save jquery
// and jQuery Definition
npm install -D #types/jquery
add jquery path in angular.json script: [] array under build like
script: [ "node_modules/jquery/dist/jquery.min.js"]
then add declare var $:any; after import statement and jquery ready to use

How can I can a mock an external class using jest?

I currently have the following Vue page code:
<template>
// Button that when clicked called submit method
</template>
<script>
import { moveTo } from '#/lib/utils';
export default {
components: {
},
data() {
},
methods: {
async submit() {
moveTo(this, COMPLETE_ACTION.path, null);
},
},
};
</script>
and then I have a test file for this page. My issue is that Im trying to check and assert that the moveTo method is called with the correct parameters using Jest. It keeps showing expected undefined but received an object. Here are the key points from the test file:
import * as dependency from '#/lib/utils';
dependency.moveTo = jest.fn();
// I then trigger the button call which calls the submit method on the page
expect(dependency.moveTo).toHaveBeenCalledWith(this, COMPLETE_ACTION.path, null);
Im unsure what this is in this context and what I should actually be passing in. Just to note I am using the mount helper from vue test utils.
I solved my issue and it was the this param within the test. This was undefined in the test and was expecting to match against a VueComponent.
I used my wrapper and then accessed the VueComponent by referencing the vm property as per the documentation: https://vue-test-utils.vuejs.org/api/wrapper/#properties
In turn I updated the following line and added wrapper.vm
expect(dependency.moveTo).toHaveBeenCalledWith(wrapper.vm, COMPLETE_ACTION.path, null);
You need to mock the module itself. In your case you are making an assertion on a spy function that is never called.
You can add a module mock by creating a "mocks/ subdirectory immediately adjacent to the module". For a node module "If the module you are mocking is a Node module (e.g.: lodash), the mock should be placed in the mocks directory adjacent to node_modules".
In your case (there are other approaches) you need to create a __mocks__ folder adjacent to the node_modules one and create a file at __mocks__/lib/utils/index.js and export the mocked function:
export const moveTo = jest.fn()

Creating custom component with <img> for markdown gatsbyjs

I'm trying to create a custom component for my markdown that accepts an image source. I am unable to display the image via the custom component because the image is not found because it doesn't exist
I also realised the image path is generated by GatsbyJS and I have no idea how to retrieve the path of the image in markdown.
I do have a custom component that holds some text but I couldn't do the same thing for images.
Here is a simple markdown with a title and a few words.
index.md
---
title: ToDoApp
---
Hi this is my todoapp app. Below is a bunch of screens
<imageholder src='./screen1.png'></imageholder>
![Image from Gyazo](./screen1.png) <!-- it displays... -->
I've created a custom component named imageholder where it holds some logic (in a near future...) in displaying the image
ImageHolder.js
import React from "react"
export default class ImageHolder extends React.Component {
render() {
return (
<img src={this.props.src} alt="Logo"/>
)
}
}
project-post.js
const renderAst = new rehypeReact({
createElement: React.createElement,
components: {
"imageholder": ImageHolder
},
}).Compiler
And I received this...
This is really tricky since (AFAIK) you can't pass props from page component to custom component with rehype-react. I think you'd need to do something similar to gatsby-remark-images, which locates the images' paths and set them.
I wrote this plugin that mimics gatsby-remark-images, but for custom components like in your case.
Here's the default setting, you can override the component name and pass in additional image transformation options.
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-custom-image-component`,
options: {
// plugin options
componentName: 'image-wrapper',
imagePropName: 'src',
sharpMethod: 'fluid',
// fluid's arguments, see gatsby-plugin-sharp docs
quality: 50,
maxWidth: 800,
}
},
],
},
},
Then use it in your markdown:
<image-wrapper src='./hero.jpg'></image-wrapper>
And get the image props in your custom component.
//src/components/ImageWrapper.js
import React from 'react'
// the result of sharp's image transformation will be passed directly to this component.
// so if you use `fluid` as `sharpMethod`, you'll get
// src, srcSet, base64, aspectRatio, srcSetType, sizes, density, originalImage.
// Please refer to `gatsby-plugin-sharp` docs.
const ImageWrapper = ({ src, srcSet }) => <img src={src} srcSet={srcSet} />
export { ImageWrapper }
The issue is that props are passed as strings to rehype - the component doesn't receive the asset hashed value when the markdown is processed and built by Gatsby. So, the prop isn't the same as the image tag's src once you build the site, and it's not finding the asset hashed file.
This plugin, Gatsby Remark Copy Linked Files, moves your referenced asset files to a public folder, and passes the correctly hashed asset path, but by default only for img, a, audio, and video tags (not for custom components).
To solve for this, move the plugin from node_modules into a /plugin folder in the project root, and add the desired custom components and props at this line. In your case, it looks like it would be:
// Handle a tags.
extractUrlAttributeAndElement($(`a[href]`), `href`).forEach(processUrl)
// Manually added custom tags
extractUrlAttributeAndElement($(`imageholder[src]`), `src`).forEach(processUrl)
Obviously this would be better served as an option for the plugin in a configuration block in gatsby-config, but this worked for me in a pinch.

How to include external JavaScript libraries in Angular 2?

I am trying to include an external JS library in my Angular 2 app and trying to make all the methods in that JS file as a service in Angular 2 app.
For eg: lets say my JS file contains.
var hello = {
helloworld : function(){
console.log('helloworld');
},
gmorning : function(){
console.log('good morning');
}
}
So I am trying to use this JS file and reuse all the methods in this object and add it to a service, so that my service has public methods, which in turn calls this JS methods. I am trying to reuse the code, without reimplementing all the methods in my typescript based Angular 2 app. I am dependent on an external library, which I cant modify.
Please help, thank you in advance.
With ES6, you could export your variable:
export var hello = {
(...)
};
and import it like this into another module:
import {hello} from './hello-module';
assuming that the first module is located into the hello-module.js file and in the same folder than the second one. It's not necessary to have them in the same folder (you can do something like that: import {hello} from '../folder/hello-module';). What is important is that the folder is correctly handled by SystemJS (for example with the configuration in the packages block).
When using external libs which are loaded into the browser externally (e.g. by the index.html) you just need to say your services/component that it is defined via "declare" and then just use it. For example I recently used socket.io in my angular2 component:
import { Component, Input, Observable, AfterContentInit } from angular2/angular2';
import { Http } from 'angular2/http';
//needed to use socket.io! io is globally known by the browser!
declare var io:any;
#Component({
selector: 'my-weather-cmp',
template: `...`
})
export class WeatherComp implements AfterContentInit{
//the socket.io connection
public weather:any;
//the temperature stream as Observable
public temperature:Observable<number>;
//#Input() isn't set yet
constructor(public http: Http) {
const BASE_URL = 'ws://'+location.hostname+':'+location.port;
this.weather = io(BASE_URL+'/weather');
//log any messages from the message event of socket.io
this.weather.on('message', (data:any) =>{
console.log(data);
});
}
//#Input() is set now!
ngAfterContentInit():void {
//add Observable
this.temperature = Observable.fromEvent(this.weather, this.city);
}
}

Categories

Resources