max-mapper / yo-yo

A tiny library for building modular UI components using DOM diffing and ES6 tagged template literals

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

trouble with basic router (sheetRouter)

SilentCicero opened this issue · comments

commented

Having trouble with this very awesome, and basic router written by: @yoshuawuyts

I figure yoyo should work fine with a basic router like this. Having trouble with the "Open" toggle. If you remove the router, it works, if you add it in, everything still works but the "Open" button no longer morphs the Main element.

(A) What does everyone think of this design pattern? (B) any thoughts on why I'm hitting this wall? (C) love yoyo, want to take this all the way :-)

The example runs in require bin:

var yo = require('yo-yo')
var sheetRouter = require("sheet-router")

var Main = function (_yield) {
  var open = true
  var el = render(_yield)

  function render (_yield) {
    return yo`
      <div>
        <h1>Layout 1</h1>
        <hr />
        <button onclick=${toggle}>Open</button>

        ${(open && "open" || "closed")}

        ${_yield}
      </div>
      `
  }

  function toggle () {
    open = !open
    yo.update(el, render(_yield));
  }

  return el
}

var Page1 = function(params) {
  var el = render()

  function render () {
    return yo`
      <div>
        <h2>Page 1</h2>
        <hr />
        <a href="/page2">Goto Page 2</a>
      </div>
      `
  }

  return el
}

var App = function(){
  var el = Main(Page1())

  var router = sheetRouter('/404', function (route) {
    return [
      route('/', function(params){ 
        yo.update(el, Main(Page1(params)));
      }),
      route('/page1', [
        route('/', function(params){ 
          yo.update(el, Main(Page1(params)));
        })
      ])
    ]
  })

  router('/')

  return el
}

document.body.appendChild(App())

You're likely losing the context of the element in Main() as the route is updating the parent which will replace the child. An easy way is to give it an identifier and document.querySelector() it to get the actual element in the DOM, not the one being created in the following DOM diff. Another option is move the reference to the element it further up the context or the open closed state further up the context.

commented

@shama what about transporting context with morphom/bel somehow with a component wrapper? It seems messy to put some of the element higher up in the context, intuitively I'd like to build components like I'm doing above, I guess I'm having trouble targeting the "el" after the parent template is updated. It seems due to my current design and dom morphing, the update/toggle functions loose the target "el". I'm wondering if there is a clean system that would keep the "el" context without dom walking with querySelector. When I use querySelector I seem to loose child elements elsewhere. I think I need to revise my knowledge on the dom and how it's being manipulated. But any thoughts on design patterns for components that do things in the bel/morphom/yoyo way, but in the philosophy of the component style above?

@chromakode I know your Diablo object does this. Although, I'll be honest, I dont want all the additional code overhead of Diablo. I like how low level yoyo is, but it seems it's almost a little too low level as I loose component targeting when yoyo updates with certain elements. Any thoughts on keeping my targeting, and element yields within a component like function architecture? I know you probably went through the paces with Diablo.

@maxogden any comments on this? What do you think is the best way I can go about building the simplest component possible with it's own state that is not affected when yo.update updates the components parent?

commented

To clarify, Ideally I'd like components to be built something like this:

const yo = require("yo-yo")

const Component = function (opts, _yield) {
   var el = render(_yield);

   function render (_yield) {
       return yo`<div> ${_yield} </div>`
   }

   function someInternalStateUpdate () {
       // some internal state change: e.g. open = true
      yo.update(el, render(_yield)) // update internal state change
   }

  return el;
}

However, as I abstract up with this design pattern, if a parent component is yo.updated, I loose the _yield within child components, and sometimes I loose the el target within the update method. I'm trying to think of ways to somehow transport the el target in the update method when a parent component is yo.updated.

I'm aware that this may not be possible with this component design pattern above. If I use ID's and querySelector, I seem to loose the _yield of the component. Which is why I dont want to use querySelector's in my component update methods.

Also, my goal is to avoid using React or Virtual dom. And I'm trying to avoid using any component frameworks, possibly just make my own very tiny one.

@SilentCicero FWIW, other dom diffing frameworks handle this by keeping the state up top. So in your example the open state gets passed into Main() and an action is sent out when it needs to update. That's where libraries like https://github.com/sethvincent/store-emitter come in handy.

But I too prefer to keep the internal updating parts of a component self contained and just use document.querySelector for those instances.

Every time Main() is called it creates a new element that is not in the DOM. That element is solely for you to use for DOM diffing or inserting into the DOM. You can't assume every new element created will be the same one that makes it into the DOM on updates as it depends on how the DOM is diffed. So when updating internally, you need to target the element that is in the DOM and not just the last element created by the function.

I experimented a bunch of using a caching system to try and retain the inDOM element context but it never worked perfectly so I scrapped it. I might return to it later but for now, document.querySelector or making the component completely state driven works for me now.

morphdom will let you know the element when it gets updated, so a more clever system could be built.

Here is an example keeping the state at the top with store-emitter: https://github.com/fraserxu/bel-todomvc

and here is an example using document.querySelector for internal updates: https://github.com/shama/fs-explorer

commented

@shama okay cool. It seems that I loose the _yield of my components when I use querySelector and id's. I'll do more experimenting and provide an example.

Also... I'm doing it: https://github.com/SilentCicero/yoyo-bootstrap =D --- Completely modular, I'll do more soon. But my hope is to build the entire react-bootstrap component system. I am using require("css"), not sure what your opinion is on this. But any browserified or webpack enabled css loading should make sense of it.

I think this is roughly the root of my problem:

const yo = require("yo-yo")

const Component = function (_yield) {
   var open = true
   var el = render(_yield)

   function render (_yield) {
       return yo`
       <div> 
         <button onclick=${toggle}>
            Component 1
         </button>

         ${open && "Open!" || "Closed!"}

         <hr />

         ${_yield}
       </div>
     `
   }

   function toggle () {
      open = !open
      yo.update(el, render(_yield))
   }

  return el
}

var comp = Component(Component(Component()))

setTimeout(function(){ 
  yo.update(comp, Component(Component(Component(Component()))));
}, 5000)

document.body.appendChild(comp)

looks like query selecting is the only way to pull this off right now:

const yo = require("yo-yo")

const Component = function (_yield) {
   var open = true
   var id = "id" + parseInt(Math.random() * 1000000)
   var el = render(_yield)

   function render (_yield) {
       return yo`
       <div id=${id}> 
         <button onclick=${toggle}>
            Component 1
         </button>

         ${open && "Open!" || "Closed!"}

         <hr />

         ${_yield}
       </div>
     `
   }

   function toggle () {
      open = !open
      yo.update(document.querySelector("#" + id), render(_yield))
   }

  return el
}

var comp = Component(Component(Component()))

setTimeout(function(){ 
  yo.update(comp, Component(Component(Component(Component()))));
}, 5000)

document.body.appendChild(comp)

I'm not sure if understand your questions, but what about a recursive signature like this. It is more complicated, but maybe the complexity could be encapsulated. In practice I have been liking the document.querySelector process. It's so simple.

var xtend = require('xtend')

module.exports = renderList

function renderList(yo, opts, prevEl) {
  var className = 'example'
  opts = xtend({
    items: [],
    onclick: function(){}
  }, opts)

  var el = yo`<div class=${className}>
    Random Numbers
    <ul>
      ${opts.items.map(function (item) {
        return yo`<li>${item}</li>`
      })}
    </ul>
    <button onclick=${onClick.bind(null, prevEl)}>Add Random Number</button>
  </div>`

  function onClick(prevEl, ev) {
    prevEl = prevEl || el
    var newEl = renderList(yo, xtend(opts, {
      items: opts.items.concat(Math.random())
    }), prevEl)
    yo.update(prevEl, newEl)
    opts.onclick(ev)
  }

  return el
}
commented

@nichoth very interesting, however, I don't think it solves my issue.

My issue as @shama describes is more like: if you make components that update themselves, you may need to use querySelector + ID's to select your component in the DOM, as the dom can get updated without updating the component's target which is pointing at a dom node that no longer exists (it's been updated), thus causing you to loose your internal node target which makes all your buttons and events not work.

I've found that just giving components an ID that is randomly generated when the component is instantiated seems to fix some of these issues.

I believe this is because the ID is generated every time the component is instantiated, causing morphdom to create a whole new dom element/children every time the component is instantiated (e.g. MyComponent()).

Not all components need this solution, but it seems that certain components need an ID assigned.

As for updating the component internally, most of the time just using the element variable seems to work. But there are some cases where I need to use ID's + querySelector.

Still investigating.. luckily I have the querySelector/random ID generation/State Management (redux/storeEmitter) at my disposal if I have trouble.

commented

@SilentCicero did you resolve this?

commented

Yes, it was a life cycle/morphing design issue. @yoshuawuyts