task-list-stencil-demo v0.0.1
Stencil in the Context of Angular vs. React
Angular or React, who cares?
Background
At this point in time, it seems that in the frontend engineering space there are two choices that development teams consider when taking on a project, React or Angular. There are other frameworks thrown into the mix: Vue, AngularJS, Preact and Surplus just to name a few. However, Angular and React largely dominate the space. With their co-existence also brings unwavering opinions about which one is better. An important question must be posed, what actually makes one better than the other?
A Developer's Preference
In my opinion, the answer to our posed question is simply that a developer prefers one over the other. Frankly, both React and Angular give you the ability to do the same thing, create progressive web applications. Angular comes with more things out of the box, like a router and an http request provider, and uses Typescript. While React is only the component layer of web applications, needs more packages added to make completed PWA's and uses JSX. Mainly I suppose the differentiator is that Angular persuades you to use some of it's out of the box features, like it's router (even though you can still change it), while React doesn't care and forces you to make those design decisions.
Scoping the Preference Discussion Down
If there is anything I have gathered, there is one main point that developers like to harp on. Some like writing components with Angular's code and template model, while others like writing components with JSX where the component logic as well as the html markdown is all encapsulated in one file. Admittedly, I started off as an Angular developer. That being said, I see the reason why people like writing components in JSX. Having everything that a component needs sitting in one file increases readability, and promotes the idea of web components being reusable at the code level (meaning that you can take a singular JSX file anywhere in a react project and have a working web component). So let's ask a different question, is there a way we can use Angular's routing, http provider, dependency injection and all the things developers love about angular, while writing our components in a React like fashion with JSX? More over, what if we could write JSX web components and reuse them anywhere, regardless of Framework? Enter StencilJS.
The Reason why Everyone is here: StencilJS
What is StencilJS
Stencil takes all the good features from multiple frameworks, like Typescipt, JSX, Reactive data-binding etc. and provides a library that allows a developer to write code that looks a lot like React to compile down to standards-based web components. These web components, more specifically called custom elements, can be used across multiple frameworks. Moreover, they can be used without a framework altogether since they are compiled to a the ECMAScript standard, allowing them to be used just like any other html tag.
Why is this post not focusing on writing Stencil components that work in React?
What I find to be rather ironic, the framework with the worst support for using standards-based web components is React. This is for two reasons; React doesn't like passing non-primitive data in to props of custom elements, and React cannot listen for DOM events coming from custom elements because it has it's own synthetic event system. Unfortuneatly, this is a huge limiting factor for using Stencil with React, and simply makes it not a wise design decision as of today. See more on framework support here: https://custom-elements-everywhere.com.
Back to the point of this post
We set up a scenario in the section Scoping the Preference Discussion Down where we, as a developer, want to use Angulars features like routing while being able to write components using JSX and look like react. With StencilJS, we are able to do so. In this post we are going to go over how to create stencil components for a task-list application, show how to publish those components as a npm package, and finally demonstrate how to use those Stencil components in an Angular application.
Environment Setup and Installing StencilJS
Text Editor
You probably can guess it. For this example I am using Visual Studio Code.
Operating System
Not that it matters a whole lot, but I am using a Windows computer to do this development. However, I am using an Ubuntu installation in Window's Subsystem for Linux, and only point this out because I think WSL is awesome and recommend the reader to look into it if developing on Windows.
Plugins for VS Code
It is worth installing the stencil-snippets (fdom.stencil-snippets) that will give you some boiler plate code for creating your stencil files. Stencil has nothing like the Angular CLI that generates files for you.
Installing Stencil
To globally install stencil, type the following in a terminal
npm install -g @stencil/core@latest
Generating a new component collection
CD into the directory you want your stencil project to live. Once there, type the following command into a terminal window.
npm init stencil
You will be given some options for setting up the project. Select component from the list to create a new component collection.
? Pick a starter › - Use arrow-keys. Return to submit.
ionic-pwa Everything you need to build fast, production ready PWAs
app Minimal starter for building a Stencil app or website
❯ component Collection of web components that can be used anywhere
Finally, specify a name for the project. In this case, I am using task-list-stencil-demo.
Pick a starter › component
? Project name › task-list-stencil-demo
At this point, Stencil will let you know that the project has been created. Go ahead and CD into the projcet.
Installing project dependencies
CD into your project folder (where package.json lives) and run the following command in a terminal.
npm install
This will install all the neccissary project dependencies for your Stencil project to run.
Running the stencil application
Finally, to run the Stencil app run the following in a terminal window.
npm start
This will open a chrome window and show you the initial state of the project.
Examining what we are given
Component File
Open the file src/component/my-component/my-component.tsx
import { Component, Prop, h } from '@stencil/core';
import { format } from '../../utils/utils';
@Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
@Prop() first: string;
@Prop() middle: string;
@Prop() last: string;
private getText(): string {
return format(this.first, this.middle, this.last);
}
render() {
return <div>Hello, World! I'm {this.getText()}</div>;
}
}
As you can see, Stencil writes a whole lot like react, and even uses the same terminology (like props). However it uses typescript, and has a @Component() directive like in angular.
Where is the 'root' of this application?
To change what we are seeing on the home page, inspect the src/index.html
file. More specifically look in the body of the HTML file.
<body>
<my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
</body>
When we create our new stencil components, we can drop them into index.html where the my-component
tag already sits.
Creating our Components
Clean-up
Let's first clean up the application and delete the my-component
folder. Also remove the line in index.html
that uses the<my-component></my-component>
tag.
Creating a new Component
Under ./src/
create a new folder called my-bulletin-add. In that folder, create a file called my-bulletin-add.tsx and another called my-bulletin-add.css. Open your .tsx file, and use that stencil-snippets extension to generate boilerplate code for a stencil component. To do this, begin typing st-component
in your .tsx file. Note: You have to add h
to the include statements to get everything to compile. Your starting component file should look like this.
import { Component, h } from '@stencil/core';
@Component({
tag: 'my-bulletin-add',
styleUrl: 'my-bulletin-add.css'
})
export class MyBulletinAdd {
render() {
return (
<p>My name is Stencil</p>
);
}
}
My-Bulletin-Add Component
We are going to create a component that has a text-box input field. This component is going to have two props, placeholderText and fontSize. placeHolderText will be a string and determine what is the placeholder for the text box when it is empty (example: enter your name). fontSize will be a number and determine what the font size of the input box. We will need an event for when the user enters a phrase into the text box, so we can emit when we have entered a form value. Lastly, we are going to need some state in the component to keep track of what has been entered into it.
Defining the props
Defining the properties of the component is very much like defining @Inputs() in Angular. The form is @Prop() propname : sometype;
. Remember to import Prop form @Stencil/core. Add the following code to you my-bulletin.tsx file above the render method.
@Prop() placeHolderText: string;
@Prop() fontSize: number;
Defining and event
Much like in Angular, Stencil makes use of an event emitter that creates a custom DOM-Event that can be captured by javascript. To create a new Event, import EventEmitter and Event from @stencil/core and add the following line of code underneath your props.
@Event() phraseEntered: EventEmitter;
Eventually we will use phraseEntered.emit()
to trigger our event.
Defining state
We need to create some component state to keep track of what is currently entered into the form. To do so, import State from @stencil/core and add the following code. Remember to give your state an initial value (an empty string).
@State() formValue: string = '';
Working in the render function
Let now create the markdown that we want to render out to the appliction. Add the following code to your component in the render()
function.
render() {
return (
<form onSubmit={(e) => console.log(e)}>
<div class="form">
<input class="add-field" placeholder={this.placeHolderText} type="text"
value={this.formValue} onInput={(event) => console.log(event)} />
<input class="add-button" type="submit" value="Submit" />
</div>
</form>
);
}
Note that this.formValue
binds our input form to the state of this component, and this.placeHolderText
assigns our prop to the placeholder of our entry form.
Next lets add the functions to deal with when the form input has changed and when the form is submitted. Modify the code to look like the following.
handleChange = (event) => {
this.formValue = event.target.value;
}
formSubmitted = (event) => {
event.preventDefault()
this.phraseEntered.emit(this.formValue);
}
render() {
return (
<form onSubmit={(e) => this.formSubmitted(e)}>
<div class="form">
<input class="add-field" placeholder={this.placeHolderText} type="text"
value={this.formValue} onInput={(e) => this.handleChange(e)} />
<input class="add-button" type="submit" value="Submit" />
</div>
</form>
);
}
In the formSubmitted()
function we emit an event with our phraseEntered
event emitter. This create a dom event that we will be able to listen for later on in Angular which will handle the data and update the component we will create next.
My-Bulletin-Item Component
This component will be a 'card' component that shows data (a string). This component will be used by our Angular applicaiton to show all the bulletin values that were entered in the my-bulletin-add component.
Create Component
Create a folder called my-bulletin-folder
under ./src/component
. In the folder create files called my-bulletin-item.tsx
and my-bulletin-item.css
that will define our component. In your .tsx file, use the st-component
snippet to generate boilerplate code for this component and remember to import h from @stencil/core.
Props
This component will have one property called content. This represents the information we want to show in this component. Be sure to import Prop from @stencil/core Add the following to your .tsx file.
@Prop() content: string;
Working in the Render function
We want to show what has been passed in as content within a div. To do this, we create a div tag with a paragraph element as it's child and include our content prop as the text contents. We also give the div a classname so we can apply some styling.
render() {
return (
<div class="content-box">
<p>{this.content}</p>
</div>
);
}
Styling
For the sake of making things look nice, add the following styles to my-bulletin-item.css
.content-box {
padding: 20px 10px;
background-color: lightgray;
}
Testing the Components
Add the following to the body of the index.html
file.
<my-bulletin-add place-holder-text="Test" font-size="12"></my-bulletin-add>
<my-bulletin-item content="some content"></my-bulletin-item>
Serve the app using the following bash command.
npm start
The application at this point should look like this.
Taking a Breather
At this point, we have created two stencil components that we want to use in an Angular applicaiton. The angular app, will recieve values submitted through the my-bulletin-add component
, add them to an array of strings (which represents our application state), then renders each string out in a my-bulletin-item component
. The components are now done, and we have to publish them to NPM so we can pull the components into an angular project.
Create a NPM profile
If you do not have one already, go to https://www.npmjs.com/ and create a new profile. This will allow you to publish pacakges to NPM and pull them into later project.
Configure NPM Login on Local Machine
To login to NPM on your local machine, in a command line type the following in a terminal
npm login
To verify that you have successfully logged in to your NPM account, type the following in a terminal
npm whoami
Configuring the Project for deployment
Open ./stencil.config.ts
and ensure it matches the following. Add your initials to the end of the namespace value to make it unique (ex. task-list-stencil-demo-dm) since NPM requires unique namespaces.
import { Config } from '@stencil/core';
export const config: Config = {
namespace: 'task-list-stencil-demo',
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader'
},
{
type: 'docs-readme'
},
{
type: 'www',
serviceWorker: null // disable service workers
}
]
};
Prepare to publish to NPM
We need to build our component collection into a distribution. To do this, run the following command in a terminal.
npm run build
This will generate minified javascript in ./dist
that will ultimately be deployed. The output of the build should look something like the following.
> task-list-stencil-demo@0.0.1 build /mnt/c/Users/dmorton/Documents/PD/stencil/task-list-stencil-demo
> stencil build --docs
[03:11.7] @stencil/core v1.4.0 💥
[03:14.9] build, task-list-stencil-demo, prod mode, started ...
[03:15.0] transpile started ...
[03:16.8] transpile finished in 1.81 s
[03:16.8] type checking started ...
[03:16.8] copy started ...
[03:16.8] generate styles started ...
[03:16.8] bundling components started ...
[03:16.9] copy finished (0 files) in 106 ms
[03:17.1] generate styles finished in 309 ms
[03:17.2] created readme docs: my-bulletin-add
[03:17.2] created readme docs: my-bulletin-item
[03:19.6] bundling components finished in 2.79 s
[03:20.4] type checking finished in 3.57 s
[03:20.6] build finished in 5.64 s
Publish to NPM
Run the following command in a terminal.
npm publish
5 years ago