Consolidate Lit repositories: figma-to-lit, figma_lit_example, lit-3d-piano, lit-calculator, lit-code-editor, lit-draggable-dom, lit-file-based-routing, lit-force-graph, lit-html-editor, lit-html-table, lit-modules, lit-native, lit-node-editor, lit-sheet-music, lit-starter-ts, lit-vscode-extension, lit-wmr, vite-lit-capacitor, vite-lit-element-starter, vite-rxdb-lit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import "@material/mwc-icon-button";
|
||||
import { iconButton } from "./app-button";
|
||||
|
||||
const TITLE = "TODOS";
|
||||
|
||||
export const appBar = (props: {
|
||||
navigation?: TemplateResult;
|
||||
title?: string;
|
||||
content?: TemplateResult;
|
||||
actions?: TemplateResult[];
|
||||
}) => {
|
||||
return html` <mwc-top-app-bar-fixed>
|
||||
${props?.navigation ?? ""}
|
||||
<div slot="title">${props?.title ?? TITLE}</div>
|
||||
${props?.actions ?? ""}
|
||||
<div>${props?.content ?? ""}</div>
|
||||
</mwc-top-app-bar-fixed>`;
|
||||
};
|
||||
|
||||
export const navigationIcon = (callback: () => void, icon: string = "menu") => {
|
||||
return iconButton({
|
||||
icon: icon,
|
||||
slot: "navigationIcon",
|
||||
callback: () => callback(),
|
||||
});
|
||||
};
|
||||
|
||||
export const actionButton = (icon: string, callback: () => void) => {
|
||||
return iconButton({
|
||||
icon: icon,
|
||||
slot: "actionItems",
|
||||
callback: () => callback(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { html } from "lit";
|
||||
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
|
||||
interface ButtonOptions {
|
||||
icon?: string;
|
||||
label?: string;
|
||||
action?: string;
|
||||
slot?: string;
|
||||
type?: string;
|
||||
form?: string;
|
||||
callback: () => void;
|
||||
}
|
||||
export const button = (options: ButtonOptions) => {
|
||||
return html` <mwc-button
|
||||
@click=${() => options.callback()}
|
||||
slot=${options?.slot ?? ""}
|
||||
label=${options?.label ?? ""}
|
||||
action=${options?.action ?? ""}
|
||||
type=${options?.type ?? ""}
|
||||
form=${options?.form ?? ""}
|
||||
icon=${options?.icon ?? ""}
|
||||
>
|
||||
</mwc-button>`;
|
||||
};
|
||||
|
||||
export const iconButton = (options: ButtonOptions) => {
|
||||
return html` <mwc-icon-button
|
||||
@click=${() => options.callback()}
|
||||
slot=${options?.slot ?? ""}
|
||||
action=${options?.action ?? ""}
|
||||
type=${options?.type ?? ""}
|
||||
form=${options?.form ?? ""}
|
||||
icon=${options.icon ?? ""}
|
||||
>
|
||||
</mwc-icon-button>`;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import "@material/mwc-dialog";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
interface DialogOptions {
|
||||
open: boolean;
|
||||
title: string;
|
||||
content: () => TemplateResult;
|
||||
dismiss: (target: HTMLElement) => void;
|
||||
}
|
||||
|
||||
export const appDialog = (options: DialogOptions) => {
|
||||
return html`<mwc-dialog
|
||||
?open=${options?.open ?? false}
|
||||
heading="${options.title}"
|
||||
@closed=${(e: any) => options.dismiss(e.target)}
|
||||
>
|
||||
<div>${options.content()}</div>
|
||||
</mwc-dialog>`;
|
||||
};
|
||||
|
||||
interface DialogActionOptions {
|
||||
label: string;
|
||||
action?: string;
|
||||
callback: () => void;
|
||||
}
|
||||
export const dialogAction = (options: DialogActionOptions) => {
|
||||
return html` <mwc-button
|
||||
slot="primaryAction"
|
||||
dialogAction="${options?.action ?? "close"}"
|
||||
@click=${() => options.callback()}
|
||||
>
|
||||
${options.label}
|
||||
</mwc-button>`;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
import "@material/mwc-drawer";
|
||||
import { Drawer } from "@material/mwc-drawer";
|
||||
|
||||
export const appDrawer = (props: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
drawerContent?: TemplateResult;
|
||||
appContent?: TemplateResult;
|
||||
open?: boolean;
|
||||
}) => {
|
||||
return html`
|
||||
<mwc-drawer hasHeader type="modal" ?open=${props?.open ?? false}>
|
||||
${props?.title ? html` <span slot="title">${props.title}e</span>` : ""}
|
||||
${props?.subtitle
|
||||
? html` <span slot="subtitle">${props.subtitle}e</span>`
|
||||
: ""}
|
||||
<div>${props?.drawerContent ?? ""}</div>
|
||||
<div slot="appContent">${props?.appContent ?? ""}</div>
|
||||
</mwc-drawer>
|
||||
`;
|
||||
};
|
||||
|
||||
export const toggleDrawer = (root: HTMLElement | ShadowRoot) => {
|
||||
const drawer = root.querySelector("mwc-drawer")! as Drawer;
|
||||
drawer.open = !drawer.open;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TemplateResult } from "lit";
|
||||
import { appBar, navigationIcon } from "./app-bar";
|
||||
import { appDrawer, toggleDrawer } from "./app-drawer";
|
||||
|
||||
export interface ScaffoldOptions {
|
||||
root: HTMLElement | ShadowRoot;
|
||||
body: () => TemplateResult;
|
||||
actions?: TemplateResult[];
|
||||
title?: string;
|
||||
}
|
||||
export const appScaffold = (options: ScaffoldOptions) => {
|
||||
return appDrawer({
|
||||
appContent: appBar({
|
||||
title: options.title,
|
||||
navigation: navigationIcon(() => toggleDrawer(options.root)),
|
||||
content: options.body(),
|
||||
actions: options.actions ? options.actions : DEFAULT_ACTIONS,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const DEFAULT_ACTIONS: TemplateResult[] = [];
|
||||
@@ -0,0 +1,45 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
export const formBuilder = (
|
||||
content: TemplateResult,
|
||||
onSubmit: (form: HTMLFormElement) => void
|
||||
) => {
|
||||
return html`<form
|
||||
@submit=${(e: any) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget as HTMLFormElement;
|
||||
onSubmit(form);
|
||||
}}
|
||||
>
|
||||
${content} <br />
|
||||
<input type="submit" value="Submit" />
|
||||
</form>`;
|
||||
};
|
||||
|
||||
export const input = (
|
||||
id: string,
|
||||
label: string,
|
||||
options?: { type?: "text" | "number"; required?: boolean }
|
||||
) => {
|
||||
return html`
|
||||
<div>
|
||||
<label for="${id}">${label}</label><br />
|
||||
<input
|
||||
id="${id}"
|
||||
type="${options?.type ?? "text"}"
|
||||
?required=${options?.required ?? false}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
export const inputValue = (
|
||||
form: HTMLFormElement,
|
||||
id: string,
|
||||
fallback: string = ""
|
||||
) => {
|
||||
const input = form.querySelector(`#${id}`) as HTMLInputElement;
|
||||
const value = input.value;
|
||||
if (value.length > 0) return value;
|
||||
return fallback;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { DEFAULT_LOADING } from "./loading";
|
||||
|
||||
export const futureBuilder = <T = any>(
|
||||
future: Promise<T>,
|
||||
builder: (result: T) => TemplateResult,
|
||||
loading: TemplateResult = DEFAULT_LOADING
|
||||
) => {
|
||||
const content = future.then((result) => builder(result));
|
||||
return html`${until(content, loading)}`;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
export const listViewBuilder = <T = any>(
|
||||
items: T[],
|
||||
builder: (item: T, index: number) => TemplateResult
|
||||
) => {
|
||||
return html` <ul>
|
||||
${items?.map((n: any, i) => {
|
||||
return html`<li>${builder(n, i)}</li>`;
|
||||
})}
|
||||
</ul>`;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { html } from "lit";
|
||||
|
||||
export const DEFAULT_LOADING = html`<span>Loading...</span>`;
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,94 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { RxDatabase, RxDocument } from "rxdb";
|
||||
import { actionButton } from "../components/app-bar";
|
||||
import { appDialog } from "../components/app-dialog";
|
||||
import { appScaffold } from "../components/app-scaffold";
|
||||
import { formBuilder, input, inputValue } from "../components/form-builder";
|
||||
import { listViewBuilder } from "../components/list-view";
|
||||
import { dbProvider, queryBuilder, Schema } from "../services/database";
|
||||
import { Todo } from "../services/todos";
|
||||
|
||||
@customElement("todo-view")
|
||||
export class TodoView extends LitElement {
|
||||
static styles = css``;
|
||||
@state() showNew = false;
|
||||
|
||||
render() {
|
||||
return dbProvider((db) => {
|
||||
db.todos.$.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
return appScaffold({
|
||||
title: "TODOS",
|
||||
root: this.shadowRoot!,
|
||||
actions: [
|
||||
actionButton("delete", async () => {
|
||||
await db.todos.remove();
|
||||
this.requestUpdate();
|
||||
}),
|
||||
actionButton("add", async () => {
|
||||
this.showNew = !this.showNew;
|
||||
}),
|
||||
],
|
||||
body: () => {
|
||||
return html`
|
||||
${queryBuilder<Todo>(db.todos.find(), (res) => {
|
||||
if (Array.isArray(res)) {
|
||||
return listViewBuilder(res, (item) => {
|
||||
return this.buildItem(db, item);
|
||||
});
|
||||
}
|
||||
return this.buildItem(db, res);
|
||||
})}
|
||||
${appDialog({
|
||||
title: "New Todo",
|
||||
open: this.showNew,
|
||||
dismiss: () => {
|
||||
if (this.showNew) this.showNew = false;
|
||||
},
|
||||
content: () => {
|
||||
return html`
|
||||
${formBuilder(
|
||||
html` ${input("title", "Title", { required: true })} `,
|
||||
async (form) => {
|
||||
await db.todos.insert({
|
||||
id: `${Date.now()}`,
|
||||
title: inputValue(form, "title"),
|
||||
});
|
||||
this.showNew = false;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
},
|
||||
})}
|
||||
`;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
buildItem(db: RxDatabase<Schema>, item: RxDocument<Todo>) {
|
||||
return html` <div>
|
||||
<input
|
||||
.value=${item.title}
|
||||
@change=${async (e: any) => {
|
||||
const value = e.target.value;
|
||||
await db.todos.upsert({
|
||||
...item.toJSON(),
|
||||
title: value,
|
||||
});
|
||||
this.requestUpdate();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@click=${async () => {
|
||||
await item.remove();
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { RxCollection, RxDocument, RxJsonSchema } from "rxdb";
|
||||
import type { TopLevelProperty } from "rxdb/dist/types/types";
|
||||
|
||||
export interface Base {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type BaseMethods = {};
|
||||
|
||||
type BaseStatics = {};
|
||||
|
||||
export type BaseCollection<
|
||||
T extends Base,
|
||||
M extends BaseMethods = BaseMethods,
|
||||
C extends BaseStatics = BaseStatics
|
||||
> = RxCollection<T, M, C>;
|
||||
|
||||
export type BaseDoc<T extends Base> = RxDocument<T, BaseMethods>;
|
||||
|
||||
export interface SchemaOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
version?: number;
|
||||
keyCompression?: boolean;
|
||||
properties: {
|
||||
[key: string]: TopLevelProperty;
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSchema<T extends Base>(
|
||||
options: SchemaOptions
|
||||
): RxJsonSchema<T> {
|
||||
return {
|
||||
title: options.title,
|
||||
description: options?.description ?? options.title,
|
||||
version: options?.version ?? 0,
|
||||
keyCompression: options?.keyCompression ?? true,
|
||||
type: "object",
|
||||
// @ts-ignore
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
primary: true,
|
||||
},
|
||||
...options.properties,
|
||||
},
|
||||
required: [
|
||||
"id",
|
||||
...Object.entries(options.properties)
|
||||
.filter((n) => {
|
||||
const [_, value] = n;
|
||||
return value.required;
|
||||
})
|
||||
.map((n) => {
|
||||
const [key, _] = n;
|
||||
return key;
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const _methods: BaseMethods = {};
|
||||
|
||||
const _statics: BaseStatics = {};
|
||||
|
||||
export function createBase<T extends Base>(options: SchemaOptions) {
|
||||
return {
|
||||
schema: buildSchema<T>(options),
|
||||
methods: _methods,
|
||||
statics: _statics,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import "@babel/polyfill";
|
||||
|
||||
import {
|
||||
addRxPlugin,
|
||||
createRxDatabase,
|
||||
RxDatabase,
|
||||
RxDocument,
|
||||
RxQuery,
|
||||
} from "rxdb";
|
||||
|
||||
// @ts-ignore
|
||||
import * as idb from "pouchdb-adapter-idb";
|
||||
import { TemplateResult } from "lit";
|
||||
import { futureBuilder } from "../components/future-builder";
|
||||
import { Todo, todosSchema } from "./todos";
|
||||
import { Base, BaseCollection } from "./base";
|
||||
import { DEFAULT_LOADING } from "../components/loading";
|
||||
|
||||
const DATABASE_NAME = "todos" + "db";
|
||||
|
||||
addRxPlugin(idb);
|
||||
|
||||
export type Schema = {
|
||||
todos: BaseCollection<Todo>;
|
||||
};
|
||||
|
||||
export var db: RxDatabase<Schema>;
|
||||
|
||||
async function setupDB() {
|
||||
if (db) return db;
|
||||
db = await createRxDatabase<Schema>({
|
||||
name: DATABASE_NAME,
|
||||
adapter: "idb",
|
||||
});
|
||||
await db.addCollections({
|
||||
todos: todosSchema,
|
||||
});
|
||||
return db;
|
||||
}
|
||||
|
||||
export const dbProvider = (
|
||||
builder: (db: RxDatabase<Schema>) => TemplateResult,
|
||||
loading: TemplateResult = DEFAULT_LOADING
|
||||
) => {
|
||||
return futureBuilder(setupDB(), (res) => builder(res), loading);
|
||||
};
|
||||
|
||||
export const queryBuilder = <T extends Base>(
|
||||
query: RxQuery<any>,
|
||||
builder: (result: RxDocument<T> | RxDocument<T>[]) => TemplateResult,
|
||||
loading: TemplateResult = DEFAULT_LOADING
|
||||
) => {
|
||||
return futureBuilder(query.exec(), (res) => builder(res), loading);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Base, createBase } from "./base";
|
||||
|
||||
export interface Todo extends Base {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const todosSchema = createBase<Todo>({
|
||||
title: "todos",
|
||||
properties: {
|
||||
title: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
});
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user