muraljs / mural

A WIP framework for React and GraphQL
MIT License
26 stars 8 forks source link

Ideas #9

Open craigspaeth opened 7 years ago

craigspaeth commented 7 years ago

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.

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 commented 7 years ago
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