{"id":21343960,"url":"https://github.com/efstajas/tela","last_synced_at":"2025-08-02T00:41:21.797Z","repository":{"id":51043486,"uuid":"258852871","full_name":"efstajas/tela","owner":"efstajas","description":"🖼🗣🌟 A small Express-based framework for building Intercom Canvas Kit applications in node.js.","archived":false,"fork":false,"pushed_at":"2024-03-28T02:23:13.000Z","size":81,"stargazers_count":18,"open_issues_count":1,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-11-10T02:13:59.318Z","etag":null,"topics":["canvas-kit","intercom","node","typescript","typescript-framework"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/efstajas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-25T19:05:27.000Z","updated_at":"2024-10-30T16:20:46.000Z","dependencies_parsed_at":"2023-01-27T20:45:24.186Z","dependency_job_id":null,"html_url":"https://github.com/efstajas/tela","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efstajas%2Ftela","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efstajas%2Ftela/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efstajas%2Ftela/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efstajas%2Ftela/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/efstajas","download_url":"https://codeload.github.com/efstajas/tela/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225824592,"owners_count":17529906,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["canvas-kit","intercom","node","typescript","typescript-framework"],"created_at":"2024-11-22T01:16:31.795Z","updated_at":"2024-11-22T01:16:32.422Z","avatar_url":"https://github.com/efstajas.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🖼 Tela \n\n![Node.js CI](https://github.com/efstajas/tela/workflows/Node.js%20CI/badge.svg)\n\nTela is a small framework for building [Intercom Canvas Kit applications](https://developers.intercom.com/building-apps/docs/canvas-kit) in node. Write your apps as simple classes, run a server to receive calls to your app with one line, and get your canvasses type-checked with TypeScript.\n\n## ⬇️ Install\n\nSimply install with npm or yarn. Tela is available on the NPM registry or GitHub Packages.\n\n```\nnpm install @efstajas/tela\n\nor\n\nyarn add @efstajas/tela\n```\n\n## 🎬 Getting started\n\nIn Tela, you define Canvas Kit apps as simple classes that match the webhooks generated by Intercom for an app. Afterwards, you can use Tela to serve your apps with one line of code.\n\n💡 *This guide assumes you use TypeScript — everything also works with plain JS too, but syntax may be different. You really should use TypeScript either way!*\n\n#### 1️⃣ Create your app\n\nCreate a new file named `your-app.app.ts`. Within that, export a new class YourName that `implements App`, and return an array consisting of a single `text` component:\n\n```ts\n//your-app.app.ts\nimport { App, Component } from '@efstajas/tela'\n\nexport default class ExampleApp implements App {\n  public initialize = (body): Component[] =\u003e {\n    return [\n      {\n        type: 'text',\n        text: 'Hello world',\n        style: 'paragraph'\n      }\n    ]\n  }\n}\n```\n\nCongratulations, you just implemented your first app. `initialize` is a `handler` — and initialize specifically is required, but you can also add `configure` and `submit` handlers if you need them. For your convenience, these handlers are a bit magic: Instead of you handling response sending and canvas creation manually in each handler, you simply return either an array of Components or a Promise that resolves to an array of Components directly. Tela will automatically take care of wrapping your components to create a valid canvas definition and send it back to Intercom once your handler has resolved. Anyway — let's run our new app!\n\n#### 2️⃣ Register your app\n\nIn a new file `index.ts`, import your app, create a new Tela instance, and register your app:\n\n```ts\n//index.ts\nimport Tela from '@efstajas/tela'\nimport YourApp from './your-app.app.ts'\n\nconst tela = new Tela()\n\ntela.registerApp('example-app', new YourApp())\n```\n\nThe first argument to `registerApp` is your app name — please make sure it's a URL-safe string, since it'll be used as a path for the server later. Please make sure you pass a new instance of your app, not the class itself!\n\n#### 3️⃣ Start the server\n\nNow that our app is registered, we can go ahead and start our server:\n\n```ts\ntela.listen(8000)\n```\n\nGo ahead and run the script — congratulations, your server is now listening on port 8000. Try calling `POST /example-app/initialize`, and you'll see that you receive back a valid canvas defininion with the `text` component you defined in your `initialize` handler.\n\nTo get your app up and running in Intercom, go read the official [Intercom Canvas Kit documentation](https://developers.intercom.com/building-apps/docs/canvas-kit). It's quite simple!\n\n## 😎 Advanced usage\n\n#### 🤚 Handlers\n\nIn each app you can define up to three handlers: `initialize`, `submit` and `configure`. To understand what they're for, it's best to read [Intercom's documentation](https://developers.intercom.com/building-apps/docs/canvas-kit). In addition to a synchronous handler like in the Getting Started guide, you can also create async handlers, for example if you need to get some data from an API to create your components:\n\n```ts\n//your-app.ts\nimport { App, Component } from '@efstajas/tela'\n\nexport default class ExampleApp implements App {\n  public initialize = async (body): Promise\u003cComponent[]\u003e =\u003e {\n    const {\n      customer\n    } = body\n    const userId = customer.id\n\n    const userData = await someApiService.getUser(userId)\n\n    return [\n      {\n        type: 'text',\n        text: `The user's favorite color is ${userData.favoriteColor}.`,\n        style: 'paragraph'\n      }\n    ]\n  }\n}\n```\n\nIf you need to read information that Intercom sends with the requests, worry not: The first argument for your handler is Intercom's request `body`.\n\n#### 🖕 Middlewares\n\nSometimes, you want to perform some logic after every incoming request and pass down some data to individual handlers. For that, you can use the `registerMiddleware` function.\n\nLet's say for example that we want to parse Intercom's `locale` context value from the incoming request, initialize an i18next instance for localization, and then pass it into each handler of our app for convenient usage.\n\n```ts\ntela.registerMiddleware(async (req, middlewareContext) =\u003e {\n  const body = req.body\n\n  const {\n    context: intercomContext\n  } = body\n\n  const browserLanguage = (intercomContext \u0026\u0026 intercomContext.locale) || 'en'\n\n  const t = await i18n(browserLanguage)\n\n  return {\n    ...middlewareContext,\n    t\n  }\n})\n```\n\nAs you can see, `registerMiddleware` accepts any handler (promise or synchronous function). Your middleware gets the full express `req` object, as well as the previous' middleware's `middlewareContext` which will include what was returned by the previous middleware in the chain.\n\nSimply perform your logic and return an object that contains all previous middleware context and the new context added by this middleware handler. If you call `registerMiddleware` multiple times, all handlers will be executed for each incoming request.\n\nWithin your app's handlers, you can now find your `middlewareContext` as part of the `context` argument.\n\n```ts\n//your-app.ts\nimport { App, Component } from '@efstajas/tela'\n\nexport default class ExampleApp implements App {\n  public initialize = async (body, context: HandlerContext): Promise\u003cComponent[]\u003e =\u003e {\n    const {\n      middlewareContext\n    } = handlerContext\n\n    const { t } = middlewareContext\n\n    return [\n      {\n        type: 'text',\n        text: t('translation.key')\n        style: 'paragraph'\n      }\n    ]\n  }\n}\n```\n\n#### 🗃 Returning stored_data and content_url\n\nIf you need to send Intercom stored data values and / or a content URL for Live Canvasses in addition to components to construct a view, you can return the more verbose `HandlerResult` or a Promise resolving to a `HandlerResult` instead:\n\n```ts\n//your-app.ts\nimport { App, HandlerResult } from '@efstajas/tela'\n\nexport default class ExampleApp implements App {\n  public initialize = async (body): Promise\u003cHandlerResult\u003e =\u003e {\n    return {\n      components: [ /* Your view… */ ],\n      storedData: {\n        foo: 'bar'\n      },\n      contentUrl: '' // Your Live Canvas Content URL\n    }\n  }\n}\n```\n\n`components` is of course required, while `storedData` and `contentUrl` are optional.\n\n#### 🌍 Handler Context\n\nAlongside the request `body` passed from Intercom, your handler also receives a `context` object as the second argument. The context includes the current app name your handler is running in, the app's base endpoint path along with two objects `hooks` and `methods`, which you can use to find out the app's other handler's endpoints at runtime.\n\n```ts\npublic initialize = (requestBody, context: HandlerContext) =\u003e {\n  const {\n    endpoint,\n    appName,\n    hooks,\n    methods\n  } = context\n\n  console.log(`\n    This handler's path is ${endpoint}.\n    It's part of app ${appName}.\n  `)\n\n  /*\n  The hooks and methods objects are helpful for registering a webhook\n  with one of your handlers at runtime, for example.\n  Let's say that in response to an action on Intercom you want to make an\n  API call that establishes a webhook to a handler in this app:\n\n  service.createWebhook({\n    url: `${hostname}${hooks.hookName.endpoint}`\n  })\n  */\n\n  return [\n    {\n      type: 'text',\n      text: 'Hello world',\n      style: 'paragraph'\n    }\n  ]\n}\n```\n\n#### 🔌 Receiving webhooks in your app\n\nOften-times, you'll need to listen to external webhooks other than those for Canvas Kit and perform some action in response. You can define external webhook handlers in a `public hooks` object:\n\n```ts\npublic hooks = {\n  hookName: (req, res, next, context) =\u003e {\n    console.log(`Handling incoming webhook at ${context.endpoint}`)\n\n    res.send(200)\n    next()\n  }\n}\n```\n\nThese handlers are defined as standard Express middleware. Each defined hook will be initialized at `/appname/hookname*` (Note the * — that means that a webhook coming in at `/appname/hookname/foobar` will still hit your handler).\n\n#### ✅ Starting the server\n\nOf course, you can run multiple apps at the same time by calling `registerApp` multiple times before calling `listen`. Each app will be initialized at `/appname/handlername`. `registerApp` also returns a Promise that resolves to a `context` object, including the paths for all handlers that were initialized within your app.\n\n```ts\nimport Tela, { HandlerContext } from '@efstajas/tela'\n// Import your apps\nimport apps from './apps'\n\nconst tela = new Tela()\n\nlet promises: Promise\u003cvoid\u003e[] = []\n\napps.forEach((app) =\u003e {\n  promises.push(\n    tela.registerApp('test', new App())\n      .then((context) =\u003e {\n        console.log('App initialized', context)\n      })\n      .catch((e) =\u003e {\n        console.error(e)\n      })\n  )\n})\n\nawait Promise.all(promises)\n\nconsole.log('All apps initialized, starting server…')\n\ntela.listen(8000).then(() =\u003e {\n  console.log(`Listening at 8000`)\n}).catch((e) =\u003e {\n  console.error(e)\n})\n```\n\n#### 🚂 Accessing the internal Express instance\n\nIf you need to add your own endpoints not part of an Intercom app to the internal server, like for example a `/health` endpoint, you can access the internal Express server instance directly. Please note that you should do this and registering apps only before calling `listen`.\n\n```ts\nimport Tela from '@efstajas/tela'\nimport App from './app'\n\nconst tela = new Tela()\n\ntela.expressInstance.get('/health', (req, res, next) =\u003e {\n  res.send(200)\n  next()\n})\n\ntela.listen(8000).then(() =\u003e {\n  console.log(`Listening at 8000`)\n}).catch((e) =\u003e {\n  console.error(e)\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fefstajas%2Ftela","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fefstajas%2Ftela","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fefstajas%2Ftela/lists"}