npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@loskir/grammy-views

v0.3.1

Published

This is an early prototype for Grammy Views — UI framework for [grammY](https://grammy.dev)

Downloads

23

Readme

Grammy Views

This is an early prototype for Grammy Views — UI framework for grammY

Grammy Views is a UI framework for Telegram bots that provides abstractions for controlling UI states.

Installation

Yarn

yarn add @loskir/grammy-views

NPM

npm i --save @loskir/grammy-views

Deno

import { View } from "https://github.com/Loskir/grammy-views/raw/master/src/mod.ts";

Comparison with other solutions

grammY Router

The Grammy Router is a basic implementation of the Finite State Machine, a concept for separating handlers into groups that are active only when the user is in a particular route.

Grammy Views enables this behavior too, but provides higher-level abstractions (e.g. local states for each View).

Telegraf Scenes

Telegraf has Scenes, a similar abstraction which was the inspiration for this library. Grammy Views uses almost the same concepts as Telegraf Scenes. The main difference is the type safety.

| Telegraf term | Grammy Views term | Description | | ------------------------- | --------------------- | ----------------------------------------------------------------------------------- | | Scene | View | Basic building block for the UI. Represents an isolated state with its own handlers | | Stage | ViewController | Middleware that registers all views and provides context flavor | | ctx.scene.session | ctx.view.state | Persistent storage that is bound to this view/scene | | scene.enter | view.onRender | Middleware that is executed upon entering the view/scene | | scene.leave | view.onLeave | Middleware that is executed upon leaving the view/scene | | ctx.scene.enter('name') | SomeView.enter(ctx) | Entering another view |

Documentation

Context flavor

You have to use ViewContextFlavor on your context in order for the types to be complete. It is additive and does not take any type parameters.

export type CustomContext = Context & ViewContextFlavor;

View

A class that represents an isolated stage of the interface with its own view (rendered with onRender middleware) and handlers (local of global). (Similar to BaseScene in Telegraf)

const SomeView = createView("some-view");

Each view must have a unique name.

Render functions

Each view can have a render function. It's called when the view is entered. Its purpose is to render the view. Usually it's done via editing a message or by sending a new one. Render functions are set via .onRender method. Several render middlewares can be applied.

const SomeView = createView("some-view");
SomeView.onRender((ctx) => ctx.reply("Hello from some view!"));

Entering a view

import { SomeView } from "./someView";

bot.command("enter", (ctx) => SomeView.enter(ctx));

Handling updates

There are 3 ways to handle updates on the view:

  • Local handlers
  • Global handlers
  • Override handlers

Local handlers

Local handlers are defined the same way as with Composer and only work when the user is inside this view.

const SomeView = createView("some-view");
SomeView.command("test", (ctx) => ctx.reply("hello!"));

bot.command("enter", (ctx) => SomeView.enter(ctx));
> /test
< // nothing
> /enter
< // now we are inside the view
> /test
< hello!

Global handlers

Global handlers are defined using .global prefix and work both inside and outside the view. They are useful for defining global entrypoints for the view.

const SomeView = createView("some-view");
SomeView.global.command("enter_some_view", (ctx) => SomeView.enter(ctx));
> /enter_some_view
< // now we are inside the view, even if we were in different view before

Override handlers

Override handlers are defined using .override prefix and only work inside the view. They have the highest priority of all three ways.

Override handlers > Global handlers > Local handlers.

Override handlers are useful for overriding other global handlers to provide similar behavior, but with some state-dependent changes.

const SomeView = createView<CustomContext, { a: string }>("some-view");
SomeView.global.command("enter_some_view", (ctx) => {
    return SomeView.enter(ctx, { a: "we came from global handler" });
});

const SomeOtherView = createView("some-other-view");
SomeOtherView.override.command("enter_some_view", (ctx) => {
    return SomeView.enter(ctx, { a: "we came from SomeOtherView" });
});

// ❌ this won't work because global handlers have higher priority than local ones
SomeOtherView.command("enter_some_view", (ctx) => {
    return SomeView.enter(ctx, { a: "we came from SomeOtherView" });
});

State

View can have state. It's used for both external data (like props) and internal data. It is defined via the second type parameter of createView function (the first is used to pass custom Context types).

const SomeView = createView<CustomContext, { a: string }>("some-view");

When entering a stateful view, it is required to pass appropriate state.

bot.command("enter", (ctx) => SomeView.enter(ctx, { a: "123" }));

View.enter method is strictly typed, so you'll get compilation error if you forgot some properties or confuse the types.

Default state

To define a default state, you use .setDefaultState method.

const SomeView = createView<CustomContext, { a: string }>("some-view")
    .setDefaultState(() => ({ a: "default a" }));

You don't have to pass properties from default state on enter (but you still can override them if you want)

// ✅ both are correct
bot.command("enter", (ctx) => SomeView.enter(ctx));
bot.command("enter", (ctx) => SomeView.enter(ctx, { a: "override" }));

Notice that .setDefaultState returns a new instance of View, so you can't call it on created instance.

// ✅ correct
const SomeView = createView<CustomContext, { a: string }>("some-view")
    .setDefaultState(() => ({ a: "default a" }));
// ❌ incorrect
const SomeView = createView<CustomContext, { a: string }>("some-view");
SomeView.setDefaultState(() => ({ a: "default a" }));

Accessing the state

View state is stored inside session an therefore is persisted between updates. It can be accessed via ctx.view.state in render middleware, local handlers and override handlers (but not in global handlers).

const SomeView = createView<CustomContext, { a: string }>("some-view");
SomeView.onRender((ctx) => {
    return ctx.reply(ctx.view.state.a); // ✅
});
SomeView.callbackQuery("a", (ctx) => {
    return ctx.answerCallbackQuery(ctx.view.state.a); // ✅
});
SomeView.override.callbackQuery("a", (ctx) => {
    return ctx.answerCallbackQuery(ctx.view.state.a); // ✅
});
SomeView.global.callbackQuery("a", (ctx) => {
    // ❌ no state here
});

ViewController

A class that provides ctx.view property in the context and manages all the activities involving views. (Similar to Stage in Telegraf)

You have to .use it on your bot instance in order to be able to use ctx.view methods. You have to register your views in this controller in order for them to work.

const viewController = new ViewController<CustomContext>();
viewController.register(
    MainView,
    OtherView,
);

bot.use(viewController);