Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ideas #9

Open
craigspaeth opened this issue Jan 19, 2017 · 1 comment
Open

Ideas #9

craigspaeth opened this issue Jan 19, 2017 · 1 comment

Comments

@craigspaeth
Copy link
Collaborator

craigspaeth commented Jan 19, 2017

Ui-Model

🤔 I wonder if we can combine a couple things used in the "controller" layer to make a useful UI model. This would be a wrapper around Baobab that makes it easy to use server/client among other useful wrappings.

  • Wraps up Lokka + convenience methods for query/mutate + update state
  • Built in bootstrapping data from server/client
  • Built in debugger/time-travel UI for dev
  • Built in persiting state to local storage/error-reporting
  • Easy way to attach to React tree or routing context

Middleware everywhere

What about the routing/controller layer being entirely a stack of koa-like middleware. For instance ctx is an object that morphs shape and tries to consolidate common abstractions. e.g. First it contains Koa server ctx stuff, then it contains Page.js ctx stuff, then any further client-side events carry that ctx and other relevant stuff like ctx.event. This could provide an easy solution for sharing things necessary across interaction e.g the state tree or cookies. It could also allow for elegant middleware that applies to all interactions such as adding a single middleware to centralize analytics tracking or authorization.

Take this concept to the extreme and you could imagine the entire application lifecycle as a bunch of middleware with routes. For instance "saving an artwork" could look like...

app.get('/artist/:id', await (ctx) => {
  const { artist, artworks } = await ctx.api.query(`{
    artist(id: ${ctx.params.id}) {
      name
    }
    artworks(artistId: ${ctx.params.id}) {
      title
    }
  }`)
  ctx.state.set({ artist, artworks })
  ctx.render(ArtistPage)
})
app.use('click', async (ctx, next) => {
  track(`Clicked ${ctx.event.target.className}`)
  await next()
})
app.on('click .artwork-save', async (ctx, next) => {
  const artwork = find(ctx.state.get('artworks'), ctx.event.target.attrs.id)
  await ctx.api.mutate(`{ saveArtwork(id: ${artwork.id) }`)
  ctx.state.select('favorites').push(artwork)
  next()
})
api.use(async (ctx, next) => {
  console.log('Saving...')
  await next()
  console.log('Saved')
})
api.on('saveArtwork', async (ctx, next) => {
  await ctx.db.atworks.save(ctx.args.id)
  await ctx.db.users.save({ favorites: { $push: ctx.args.id } })
  next()
})

I guess you couldn't reasonably share ctx b/t app and api here, but potentially app server-side ctx could be partially shared to the client, then shared across the rest of client-side interactions.

This idea of a universal controller middleware stack could allow for a powerful low level abstraction that can be built upon to provide universal routing, analytics, rendering, and more.

const track = (ctx, next) => {
  await next()
  if (ctx.url && !ctx.browser) analytics.track(`Loading page ${ctx.url}`)
  else if (ctx.url) analytics.track(`Loaded page ${ctx.url}`)
  else if (ctx.event) analytics.track(`Clicked ${event.target.className}`)
}

const index = async ({ state, render }) => {
  const { todos } = await api.query(`{ todos { _id body } }`)
  state.set({ todos })
  render(Body)
}

const removeTodo = async ({ api, _id }) => {
  await api.mutate(`{ deleteTodo(_id: "${_id}") { _id } }`)
  const todos = reject(state.get('todos'), { _id })
  state.set({ todos })
}

const addTodo = async ({ event, api, state }, next) => {
  if (event.key !== 'Enter') return next()
  const { createTodo: todo } = await api.mutate(`{
    createTodo(body: "${event.target.value}") { _id body }
  }`)
  state.select('todos').push(todo)
}

controller.use(track)
controller.get('/', index)
controller.on('removeTodo', removeTodo)
controller.on('addTodo', addTodo)

View

({ artwork }) => 
  li(
    img({ src: artwork.src })
    button({ ref: 'artworkHeart', data: { artwork } }, '♡'))

Router

router.on('click artworkHeart', saveArtwork)

Controller

async (ctx, next) => {
  const artwork = ctx.event.target.data.artwork
  ctx.state.favorites.push(artwork)
  try { await ctx.api.mutate(`{ updateUser(favorites: ${ctx.state.favorites}) }`) }
  catch (e) { ctx.state.favorites.pop() }
}

Model

user.on('update favorites', (ctx, next) => {
  await next()
  const partnerIds = await ctx.res.favorites.map('partnerId')
  const partners = await ctx.db.partners.find({ id: { $in: partnerIds } })
  partners.each(sendMail)
})
@craigspaeth
Copy link
Collaborator Author

craigspaeth commented Jan 22, 2017

const controller = universalController()

const controller.use((ctx, next) => {
  // Universal: General
  ctx.state
  ctx.params
  ctx.render(View)
  ctx.emit('')
  ctx.api // Lokka
  ctx.bootstrap(() =>)

  // Browser/Server: Routing
  ctx.url
  ctx.origin
  ctx.path
  ctx.query
  ctx.querystring
  ctx.host
  ctx.protocol
  ctx.secure
  ctx.href
  ctx.subdomains
  ctx.cookies.(get|set)
  ctx.redirect(path)
  ctx.headers // Can we consolidate things like navigator.userAgent here?

  // Environment
  ctx.env // browser|server|nativer...
  ctx.server... // Koa ctx?
  ctx.browser...  // Page.js ctx?
  ctx.native // Something about React Native?
})

export const removeTodo = async ({ state, { _id } = params }) => {
  state.set({ removingTodo: _id })
  await api.mutate(`{ deleteTodo(_id: "${_id}") { _id } }`)
  state.set({
    removingTodo: null,
    todos: reject(state.get('todos'), { _id })
  })
}

const log = async (ctx, next) => {
  const start = new Date().getTime()
  console.log(`Sending from ${ctx.url}...`)
  await next()
  console.log(`Took ${new Date().getTime() - start}ms`)
}

controller.use(log)
controller.get('/img/:id', resizeImg)
controller.get('/', renderIndex)
controller.delete('/todo/:id', removeTodo)
controller.on('removeTodo', removeTodo)

// View
li(
  button({ onClick: send('removeTodo', { _id: todo._id }) }, 'X'))

// Client
controller() // Client, Attaches to DOM

// Server
app.use(controller()) // Server, Mounted in Koa

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant