Tungsten

Tungsten: Simple, TypeScript first, React-like Front End Framework

Tungsten is an experimental front end framework, similar to react. It began when I was dissatisfied with some aspects of react - especially the lack of a styling solution I was entirely happy with - I wanted to be able to create stylesheets for my components, in my component file (too much time searching for files otherwise), that was fully scoped. Svelte today has this, while it is not on offer for React. Tungsten today also has this.

I started out with some tweaks on top of React, that went well, but then decided to build it out from scratch to refine it further.

I have decided to match the workings of the framework to React, and support existing React syntax, which makes porting very quick (aside from a few features not yet supported).

I am starting to use the framework for projects, and seeing how it goes.

Key features:

Create a simple counter component

import { StatefulComponent } from 'tungsten'
type CounterProps = object
type CounterState = {
count: number
}
class Counter extends StatefulComponent<CounterProps, CounterState> {
state: CounterState = {
count: 0
}
content = () => (
<div>
<h1>{this.state.count}</h1>
<button onClick={ () => this.state.count++ }>+</button>
</div>
)
style = Sass`
div
background: black
h1
color: white
button
color: white
background: purple
`
}
And, here is that counter, with a little extra styling:

0

Type-safe binding

Let's go
import { StatefulComponent, BindableInput } from 'tungsten'
import { InputKeyboardEvent } from 'tungsten/types/jsx/html.events.js'
type State = {
name: string
age: number | undefined
}
export class Binding extends StatefulComponent<object, State> {
state: State = {
name: '',
age: undefined
}
numberize = (newValue: string, event: InputKeyboardEvent) => { // filter a value to a number or undefined
let n: number | undefined = parseFloat(newValue)
if (isNaN(n)) n = undefined
return n // return the filtered value, which eventually will be set to this.state.age
}
content = () => (
<>
<form>
<BindableInput placeholder="name" bind={[this.state, 'name']} /><br />
<BindableInput placeholder="age" bind={[this.state, 'age']} filter={this.numberize} />
</form>
<div>Name: {this.state.name}</div>
<div>Age: {this.state.age}</div>
</>
)
}
Which gives us:
(note: this is using the vanilla OnChange event, so will require clicking out to update. work on events to come.)

Name:
Age:

'bind='' takes a type of [object, string]. In this example we bind to 'this.state.x', but we could bind to other variables such as this.x, or arbitrary objects. Of course, only binding to state will trigger component updates. filter= allows is to specify a function to process the value returned, and can be used to prevent or change input.

Easy type-safe global store

We can create a type-safe global store, by:
1. creating a class that describes the shape of our store, with default value, called the schema.
2. Calling the 'registerStore' to register the schema, which returns a function to get the store for thecurrent app instance 3.
3. Defining a StoreComponent class that extends a base class, and loads the store on initialization.
This code will exist in the starter template.
import {
type ComponentPropsBase, type ComponentStateConstraint,
registerStore, BaseSimpleStoreComponent, BaseStatefulStoreComponent
} from 'tungsten'
// define a store schema, specifying entry types and default values
class StoreSchema {
username: string = 'Bob'
profilePicture: string = '/images/profile-picture.jpg'
}
// leave this as-is
const getStore = registerStore(StoreSchema)
export abstract class SimpleStoreComponent<
Props extends ComponentPropsBase = object,
RouteData = undefined
> extends BaseSimpleStoreComponent<Props, RouteData, StoreSchema> {
store = getStore(this)
}
export abstract class StatefulStoreComponent<
Props extends ComponentPropsBase = object,
RouteData = undefined
> extends BaseStatefulStoreComponent<Props, RouteData, StoreSchema> {
store = getStore(this)
}
The usable store is formed from the schema. Each entry 'entry = initialValue' is transformed into (psuedocode):
entry: {
value: initialValue,
get = () => value,
set = (newValue: Type) => value = newValue,
subscribe = () => 'subscribes to updates',
sync = () => {
'subscribes to updates'
return value
}
}
Once we have the store schema and base class defined, we can easily create a new component with store access
import { StoreComponent } from '../Store.js'
export class Profile extends StoreComponent {
content = () => <section>
<img src={this.store.profilePicture.sync()} /><br />
<span>{this.store.username.sync()}</span>
</section>
style = Sass`
img
height: 4em
`
}
By using
this.store.profilePicture.sync()
this.store.username.sync()
we get the current values for those store entries, and also cause the Profile to update with the new values if they are changed.
<Profile /><br />
<UpdateProfile /><br />

Bob


Function Components

Function components for simple components are supported. Support for a wider array of hooks is a (low priority) work in progress.

type Props = {
name: string
}
export const Hello = ({name}: Props) => (
<h3>Hello {name}!</h3>
)
Hello.style = Sass`
h3
color: blue
`
<Hello name="Jim">

Hello Jim!

More

The client is currently 44.5KiB minified, uncompressed, however features are still being added.


Read the complete docs (WIP)