Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kayahr committed Jan 22, 2025
0 parents commit a88bbe6
Show file tree
Hide file tree
Showing 109 changed files with 14,701 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .cspell/project-words.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
importmap
jsxdev
jsxs
kayahr
lifecycles
multilines
Reimer
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# http://editorconfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.{html,xml}]
indent_size = 2
2 changes: 2 additions & 0 deletions .github/funding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github: kayahr
custom: https://paypal.me/kayaahr/
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 22.x ]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build
run: npm ci
- name: Test
run: npm test
- name: Build API Doc
run: npm run apidoc
- name: Deploy API Doc
if: ${{ github.ref == 'refs/heads/main' && matrix.node-version == '22.x' }}
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: lib/apidoc
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/lib
/node_modules
/*.tgz
sandbox
/dist
16 changes: 16 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"files.exclude": {
"lib": true,
"node_modules": true
},
"task.allowAutomaticTasks": "on",
"javascript.preferences.importModuleSpecifierEnding": "js",
"typescript.preferences.importModuleSpecifierEnding": "js",
"typescript.preferences.autoImportFileExcludePatterns": [
"@kayahr/harmless",
"@kayahr/harmless/jsx-runtime",
"./src/main/index.ts",
"./src/main/jsx-runtime.ts"
]
}
29 changes: 29 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile and watch project",
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": {
"kind": "build",
"isDefault": true
},
"runOptions": {
"runOn": "folderOpen"
}
},
{
"label": "Run unit tests",
"command": "${command:testing.coverageAll}",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}
22 changes: 22 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT LICENSE
===========

Copyright (C) 2024 Klaus Reimer, k@ailis.de

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[GitHub] | [NPM] | [API Doc]

| :warning: This project is currently under construction and missing crucial functionality |
| - |

A minimalistic reactive web frontend framework written in TypeScript.

## Features

* Fine-grained reactive DOM updates via [promises], [observables] and signals using a framework-independent [signal] implementation.
* Based on standard [JSX] using the automatic runtime (aka `react-jsx` mode), so no special transpiler plugin needed.
* Supports dependency injection via a framework-independent [cdi] implementation.
* Provides built-in components for flow control, like [If], [Choose] and [Route].
* It's just a library without any build system requirements. Use whatever you like.
* Easily testable with any JSX-capable testing framework (like [Vitest]).

Some features are intentionally missing to keep Harmless small and focused:

* No server rendering. Harmless is a client-only library.
* No web component support. Should be easy enough to use Harmless inside a web component, though.
* No CLI tools. Harmless is just a library and doesn't dictate how to structure your project or how to work with it.
* No CommonJS support. It's time to leave the stone age behind and use [ESM] everywhere.

## TODO

* Write built-in components like `For` to iterate over collections of data.
* Write documentation

## More

Check out the [documentation].


[API Doc]: https://kayahr.github.io/harmless/modules/_kayahr_harmless.html
[Documentation]: https://kayahr.github.io/harmless/documents/Getting_started.html
[GitHub]: https://github.com/kayahr/harmless
[NPM]: https://www.npmjs.com/package/@kayahr/harmless
[Vitest]: https://vitest.dev/
[signal]: https://www.npmjs.com/package/@kayahr/signal
[cdi]: https://www.npmjs.com/package/@kayahr/cdi
[promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[observables]: https://github.com/tc39/proposal-observable
[JSX]: https://www.typescriptlang.org/docs/handbook/jsx.html
[ESM]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
[If]: https://kayahr.github.io/harmless/documents/Control_Flow.If.html
[Choose]: https://kayahr.github.io/harmless/documents/Control_Flow.Choose.html
[Route]: https://kayahr.github.io/harmless/documents/Control_Flow.Routes.html
8 changes: 8 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
TODO
====

* There is a bug somewhere with fragments. Most likely reproducible with nested Show components which all use fragments. Noticed this with the Route
component when switched child component is a fragment.
*
* Implement Route
* Implement For
22 changes: 22 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"version": "0.2",
"language": "en",
"files": [ "**/*" ],
"dictionaryDefinitions": [
{
"name": "project-words",
"path": "./.cspell/project-words.txt",
"addWords": true
}
],
"dictionaries": [ "project-words" ],
"ignorePaths": [
"./.cspell",
"./.git",
"./lib",
"./node_modules",
"./package-lock.json",
"sandbox"
]
}
142 changes: 142 additions & 0 deletions doc/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: Components
children:
- ./components/function-components.md
- ./components/class-components.md
- ./components/rendering.md
- ./components/reactive-content.md
- ./components/component-lifecycles.md
- ./components/event-handlers.md
- ./components/dependency-injection.md
---

# Components

Components are written in form of simple functions or in form of [class components]. Both are equal in functionality so use whatever coding style you prefer. The examples in this documentation concentrates on functions but the shown concepts always also work with classes.

Component names should always be upper-camelcase like `Contact` and `ToggleButton` because lower-case element names are reserved for normal HTML elements like `div`, `span` and all the others (also called intrinsic elements in the JSX world).

## Basics

The most basic form of a component is a function without parameters which just returns
static HTML:

```typescript
export function HelloWorld() {
return <h1>Hello World<h1>;
}
```

Other components can now import this component and use it in their own template:

```typescript
import { HelloWorld } from "./HelloWorld.js";

export function App() {
return <div><HelloWorld /></div>;
}
```

## Properties

Properties are passed in as the first function argument (or first constructor argument when using classes) in form of a plain key/value object. When the component doesn't expect such an argument then the component does not allow any properties.

The following example shows the component `Contact.tsx`. It has two properties: `firstName` and `lastName`.

```typescript
export function Contact(props: { firstName: string, lastName: string }) {
return <div class="contact">
<div>First name: {props.firstName}</span>
<div>last name: {props.lastName}</span>
</div>;
}
```

It can be used in other components like this:

```typescript
import { Contact } from "./Contact.js";

export function Contacts() {
return <div class="contacts">
<Contact firstName="Tricia" lastName="McMillan" />
<Contact firstName="Arthur" lastName="Dent" />
</div>;
}
```
There are no limitations on component properties. Just treat them as you would treat a standard `object` type in TypeScript. Properties can be of any type and may also be optional. You can also use destructuring with default values and provide a properties interface to make the usage more pleasant:
```typescript
interface UserProperties {
name: string;
id: number;
admin?: boolean;
}

function User({ name, id, admin = false }: UserProperties) {
return <li class="user">{name}#{id}{admin ? " (Admin)" : ""}</li>;
}

function Users() {
return <ul>
<User name="root" id={0} admin={true} />
<User name="arthur" id={1000} />
</ul>
}
```

## Children

Component children are passed as `children` property to a component. If the component does not specify such a property then children are not allowed. So to use children you have to explicitly specify them like this:

```typescript
function Bold(props: { children: JSX.Element }) {
return <b>{props.children}</b>;
}
```
In most cases you want to use `JSX.Element` as children type to pass through any supported type and any number of children.
For special use-cases the type can be narrowed down. Let's say you write a component which expects a list of numbers as children:
```html
<Numbers>
{1}
{2}
{3}
</Numbers>
```
Note that JSX treats multiple children differently than a single child or no child at all. So you won't get an empty array, an array with one number or an array with multiple numbers. Instead you will get `undefined` when no child is specified, `number` when a single number is specified, and `number[]` when multiple numbers are given. Therefor an implementation accepting any number of values (even none) must be written like this:
```typescript
function Numbers({ children }: { children?: number | number[] }) {
...
}
```
## More
* [Function Components]
* [Class Components]
* [Rendering]
* [Reactive Content]
* [Component Lifecycles]
* [Event Handlers]
* [Dependency Injection]
## Seel also
* [TypeScript's JSX documentation](https://www.typescriptlang.org/docs/handbook/jsx.html)
[signals]: https://www.npmjs.com/package/@kayahr/signal
[promises]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[observables]: https://github.com/tc39/proposal-observable
[Class Components]: ./components/class-components.md
[Function Components]: ./components/function-components.md
[Rendering]: ./components/rendering.md
[Reactive Content]: ./components/reactive-content.md
[Component Lifecycles]: ./components/component-lifecycles.md
[Event Handlers]: ./components/event-handlers.md
[Dependency Injection]: ./components/dependency-injection.md
39 changes: 39 additions & 0 deletions doc/components/class-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Class Components
---

# Writing a class component

And here is the same component written as a class:

```typescript
export interface ContactProperties {
firstName: string;
lastName: string;
}

export class Contact {
public constructor(private readonly props: ContactProperties) {}

public render() {
return <div class="contact">
<div>First name: {this.props.firstName}</span>
<div>Last name: {this.props.lastName}</span>
</div>;
}
}
```

If you prefer explicit return types and prefer specifying the implemented interface you can use the `JSX.Element` and `JSX.ElementClass` types:

```typescript
import { JSX } from "@kayahr/harmless";

export class Contact implements JSX.ElementClass {
...
public render(...): JSX.Element {
...
}
...
}
```
7 changes: 7 additions & 0 deletions doc/components/component-lifecycles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: Component Lifecycles
---

# Component Lifecycles

TODO
Loading

0 comments on commit a88bbe6

Please sign in to comment.