Quickstart

This is a fast whirlwind tour for all you developers tipping their toes into modern web development. First things first, if you're reading this at night or other dark light conditions, click on the "A" icon on top of the page and tune the color theme to sepia or night. Docs being easy on the eyes helps to get into a more relaxed state.

Installation

Seagull builts on top of JavaScript, so you need node.js installed. I'll assume that you are developing on Mac OS X and have homebrew installed:

# node version manager, like rvm or rbenv for ruby
$ brew install nvm

# reload current shell with tweaked environment for nvm
$ echo "source $(brew --prefix nvm)/nvm.sh" >> ~/.profile

# latest LTS version which AWS Lambda supports (import for later deployments)
$ nvm install 6.10.3

# always use this node.js version as default in new shells
$ nvm alias default 6.10.3

For other cases, download node.js with your package manager of choice or head over to node.js official downloads page and get the latest LTS version 6.X.

Once you have node.js on your machine, installing seagull is as easy as installing any global NPM package:

$ npm install -g @seagull-js/seagull-cli

This CLI tool is now available on your machine as seagull and contains all tooling you'll need for developing seagull apps.

Optional: VSCode

Of course you're totally free to choose any code editor or IDE for coding on seagull projects, the recommended code editor is Visual Studio Code (VSCode). It has strong support for typescript and tsx out of the box, plus ships with a native git integration and looks good by default.

Scaffolding

This may come as a shocking surprise for you, but we will create a simple ToDo- App. By keeping the problem domain super simple, there is more room to highlight some technological details and still get something up and running in a few minutes. Having the elephant in the room out of the way, let's go:

# you probably have s special place on your disk for coding stuff, right?
$ cd path/to/your/projects/folder

# scaffold a new app from scratch
$ seagull new todos

# change into the directory of the new app, optionally fire up VSCode
$ cd todos && code .

All basic files and configurations are generated your you, as well as app dependencies resolved (they reside in node_modules/). If you inspect the central package.json file, you'll see only a light dependency on the Seagull core. The heavy lifting of toolchains is outsourced into the CLI tool you just installed above, which means your actual app is featherlight. Okay, let's see that everything worked out correctly:

$ seagull dev

This starts a local development server on localhost:3000/. If you navigate your browser there, you should see a classical "hello world" message. You can stop this dev server any time with the key kombination Ctrl + C.

Frontend-Layout

Now let's build some frontend stuff. You should have some basic knowledge about what a SPA is, how react works and JSX files look like and have an idea about the latest ECMAScript/Typescript goodies.

First things first, we update the generated frontend/layout.tsx file to look like this:

import createElement from 'inferno-create-element'

// this is what your index.html looks like
export default function Layout({ children }) {
  return (
    <html>
      <head>
        <title>Seagull ToDo App</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
      </head>
      <body>
        <div className="container">
          <h1>Seagull ToDo App</h1>
          <div id='root' className="">{children}</div>
        </div>
        <script src="/assets/bundle.js"></script>
      </body>
    </html>
  );
}

This is the equivalent of "index.html" files in other frameworks. We added Bootstrap3 to it for some basic styles and responsiveness. Note that in TSX, every tag must be closed properly (even the link in the head section) and the class attribute of HTML is called className. Since TSX is just Javascript in the end, the word "class" has a special meaning in JS already, so this got mapped. If you reload the page in your browser, you should notice the changes (you still have seagull dev running, have you?).

Frontend-State

We need some model to represent a single todo item. The next steps are a bit of data plumbing and show you how easy it is to wire things up. Create a file frontend/models/todo.ts with the following content:

// the reactive state management library of choice
import { observable } from 'mobx'

// this is a single todo item
export default class Todo {
  // id will never change after creation
  id: string

  // checked will change on user interactions
  @observable checked: boolean = false

  // text will never change after creation
  text: string

  // create a new instance from a given text and create a uuid
  constructor (text: string) {
    this.text = text
    this.id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      const r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8)
      return v.toString(16)
    })
  }
}

This is just a ordinary typescript class. An important note is the mobx-import on top: we will use a reactive state management library called MobX for the frontend. Seagull ships with MobX already, so there is no need for installing it on your own. We also decorate the checked property as observable, which means that whenever this property changes, some part of the UI might re-render. Because we want to reference individual todo items later on, we generate a uuid on creation.

Now that we have an idea about how a todo item looks like, we need a store which manages a list of todo items. This store should also include just enough functionality to perform the basic operations needed: add a new todo, toggle specific todos as checked or unchecked, delete a specific todo. Create a file frontend/stores/todos.ts:

import { observable } from 'mobx'
import Todo from '../models/todo'

export default class TodoStore {
  @observable list: Todo[] = []

  constructor() {
    this.addTodo('wake up')
    this.addTodo('go shopping')
    this.addTodo('buy milk')
  }

  addTodo (text: string) {
    this.list.push(new Todo(text))
  }

  deleteTodo (id: string) {
    const index = this.list.findIndex(todo => todo.id === id)
    this.list.splice(index, 1)
  }

  toggleTodo (id: string) {
    const index = this.list.findIndex(todo => todo.id === id)
    this.list[index].checked = !this.list[index].checked
  }
}

For a bit more convenience, some todos are preloaded on creation. Note how this again is just an ordinary typescript class, with the small addition that the list property is decorated as observable.

Frontend-Routing

Until now, these files are not used yet in the demo frontent, so let's change that. All the wiring happens in the frontend/routes.tsx file, which you should modify to look like this:

// library imports
import { history } from '@seagull-js/seagull'
import createElement from 'inferno-create-element';
import { Provider } from 'inferno-mobx'
import { Route, Router } from 'inferno-router';

// import of stores
import Todos from './stores/todos'

// import of individual pages
import HelloPage from './pages/hello'

// routing structure
const routes = (
  <Provider todos={ new Todos() }>
    <Router history={ history }>
      <Route path='/' component={ HelloPage }/>
    </Router>
  </Provider>
)

export default routes

Here we just imported our todos store and made it available to all routes and their components by wrapping all with a Provider. If you have more stores, just add more attributes to the one Provider.

Now that the data and business logic is in-place, it is time to build the actual components!

Frontend-Components

We start with the individual todo item, which will be rendered within a table. Create a file frontend/components/todo_item.tsxwith this content:

import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import { connect } from 'inferno-mobx'
import Todo from '../models/todo'
import Todos from '../stores/todos'

interface IProps {
  todo: Todo,
  todos: Todos
}

@connect(['todos'])
export default class TodoItem extends Component<IProps, {}> {
  render() {
    return (
      <tr>

      {/* column 1: the checkbox for the 'checked' state */}
      <td>
        <input
          type="checkbox"
          checked={ this.props.todo.checked }
          onClick={ e => this.props.todos.toggleTodo(this.props.todo.id) }
        />
      </td>

      {/* column 2: the actual text, either striked through or not */}
      <td>{ this.props.todo.checked ? (
        <strike>{ this.props.todo.text }</strike>
      ) : (
        <span>{ this.props.todo.text }</span>
      )}</td>

      {/* column 3: a delete button for this todo item */}
      <td>
        <button
          onClick={ e => this.props.todos.deleteTodo(this.props.todo.id) }
          className="btn btn-danger btn-xs"
        >delete</button>
      </td>

    </tr>
    )
  }
}

The Component class from which we're deriving here is generic over:

  • the props we're passing into the component (here: IProps)
  • the internal state the component will hold (here: {}, which means none)

We will pass in a specific Todo, and the connect decorator will extend the props to include the exact store we just provided in the routes.tsx above. This is the central magic of the frontend with inferno. No need for complex Flux architectures or setting up massive amounts of boilerplate, there are just (reactive) stores, components, and some connection between them.

Also note that since we defined our todos store explicitly within IProps, we get fully typesafe intellisense while writing this all.

Now we're increasing the complexity a bit. We need a component which contains an text input field with a submit button, which triggers the addTodo method of our todos store. Create a file frontend/components/input_form.tsx:

// external library imports
import createElement from 'inferno-create-element';
import Component from 'inferno-component';
import { connect } from 'inferno-mobx'
import Todos from '../stores/todos'

// external data that gets passed into this component
export interface IProps {
  todos: Todos
}

// internal 'state' data structure of this component
export interface IState {
  input: string
}

// the (stateful) component for the page with type checking
@connect(['todos'])
export default class InputForm extends Component<IProps, IState> {
  constructor(props) {
    super(props)
    this.state = { input: '' }
  }

  setInputText = (event) => this.setState({ input: event.target.value })

  invoke = (_event) => {
    this.props.todos.addTodo(this.state.input)
    this.setState({ input: '' })
  }

  render() {
    return (
      <div className="input-group">
        <input
          className="form-control"
          value={ this.state.input }
          onChange={ this.setInputText }
          placeholder="enter some todo text..."
        />
        <span className="input-group-btn">
          <button className="btn btn-default" onClick={ this.invoke }>add todo</button>
        </span>
      </div>
    )
  }
}

We will keep the value of the input field in sync with the internal state of the component, and on button click trigger our store method and reset the input afterwards. The difference compared with the above component is just that we here use a bit of an internal state (statically typed as IState) for some component-local caching of a value until we really need it.

Now there is only one last step to do: updating our frontend/pages/hello.tsx page to show us the new logic. Add the following code:

import Component from 'inferno-component';
import createElement from 'inferno-create-element';
import { connect } from 'inferno-mobx'
import Todos from '../stores/todos'
import InputForm from '../components/input_form'
import TodoItem from '../components/todo_item'

interface IProps {
  todos: Todos
}

// the (stateful) component for the page with type checking
@connect(['todos'])
export default class HelloPage extends Component<IProps, {}> {

  render() {
    return (
      <div>
        <table className="table table-bordered table-hover">
          <thead>
            <tr>
              <td>Status</td>
              <td>Text</td>
              <td>Actions</td>
            </tr>
          </thead>
          <tbody>
            {this.props.todos.list.map((todo, index) =>
              <TodoItem todo={ todo } />
            )}
          </tbody>
        </table>
        <InputForm />
      </div>
    )
  }
}

And reload your browser. You should see a fully functional Todo-App in front of you! Congratulations, you just mastered the probably most difficult part of modern web development: building complex single page apps.

Backend-SSR

As you might know, a SPA is rendered dynamically on the browser, which means that the browser must load some initial html file with a bunch of scripts, execute those scripts, optionally fetch data from an API and then renders the UI. Quite a few steps until a user can "see" something. And what about old school bots and scrapers, what do they see? the initial (empty) HTML? Does Google really execute Javascript while crawling (they do, a bit) and do they wait for data fetching before pagers can be rendered (currently: mostly not)?

These are the actual weak points of a SPA: initial load time for users and crawlability for bots. The solution for these issues is called SSR. Since Seagull builds upon node.js for all things backend, it can natively execute the todo frontend you just built above. Need a proof? Just click the view source button in your browser or run curl http://localhost:3000/ in a terminal. There is fully rendered HTML, instantly!

You didn't have to configure anything by yourself, the magic happens within the api/Frontend.ts api handler which got generated when you started the project with seagull new ....

Deployment

todo

results matching ""

    No results matching ""