-
Notifications
You must be signed in to change notification settings - Fork 65
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
querySelector/ID component design #15
Comments
Looks like a good component structure, especially as it's still just returning native nodes. So others could consume it without issue. It's got me thinking though, If that works, then components wouldn't have to concern themselves with generating an id. |
@shama you read my mind.. no joke working on something like this right now Okay, can you draw up some sudo code, I'm thinking the same thing. The mutation observer seems to provide more than enough facility. But my worry is just loosing track. Right now I'm dom walking to see changes in nodes added, removed and mutated. I'm using a data-set ID, but I'm setting the id within the component, this is so that I can grab the component later on when I update the component. It kinda works, but seems to loose the element once the entire app is updated a few times. Walking the added nodes in mutation observer seems to miss a couple new nodes, not sure why. const yo = require("yo-yo")
var watch = {}
var components = {}
function walkChildren (n, v) {
v(n); for (var i = 0; i < n.childNodes.length; i++) walkChildren(n.childNodes[i], v)
}
if (window && window.MutationObserver) {
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var mutation = mutations[i]
/*if(mutation.target.dataset
&& mutation.target.dataset.yoid
&& typeof components[mutation.target.dataset.yoid] !== "undefined")
components[mutation.target.dataset.yoid].mutated(mutation.target)*/
console.log(mutation);
if(mutation.target.dataset
&& mutation.target.dataset.yoid
&& typeof components[mutation.target.dataset.yoid] !== "undefined")
components[mutation.target.dataset.yoid] = mutation.target;
var x;
for (x = 0; x < mutation.addedNodes.length; x++) {
walkChildren(mutation.addedNodes[x], function(node){
if(!node.dataset) return; if(!node.dataset.yoid) return
console.log(node.dataset.yoid);
if(!components[node.dataset.yoid]) components[node.dataset.yoid] = node // onmutation(node.dataset.yoid)
//components[node.dataset.yoid].node = node
//components[node.dataset.yoid].added(node)
});
}
for (x = 0; x < mutation.removedNodes.length; x++) {
walkChildren(mutation.removedNodes[x], function(node){
if(!node.dataset) return
if(!components[node.dataset.yoid]) return
//components[node.dataset.yoid].removed(node)
//delete components[node.dataset.yoid]
});
}
}
})
observer.observe(document.body, {childList: true, subtree: true})
}
function onmutation (yoid, l, u, m) {
//if(!components[yoid]) components[yoid] = {}
//components[yoid].added = l || function () {}
//components[yoid].removed = u || function () {}
//components[yoid].mutated = m || function () {}
}
function getComponent(id) {
return components[id];
}
const Component = function (_yield) {
var id = "id"+ parseInt(Math.random()*1000000);
var open = true
function onload (node) {
console.log(id + "loaded!")
}
function onupdate (node) {
console.log(id + "mutated")
}
function onunload (node) {
console.log(id + "unloaded!")
}
function toggle () {
open = !open;
console.log(components, components[id])
yo.update(getComponent(id), render(_yield))
}
function render (_yield) {
return yo`
<div data-yoid=${id}>
<button onclick=${toggle}>${id} - Toggle</button>
<span> ${open && yo`<div> It's Open </div>` || yo`<div> It's Closed </div>`} </span>
<hr />
${_yield}
</div>
`
}
onmutation(id, onload, onunload, onupdate)
return render(_yield)
}
var app = yo`
<div>
${Component(Component(Component(Component())))}
${Component(Component(Component(Component(Component()))))}
${Component(Component(Component(Component())))}
</div>
`
document.body.appendChild(app); |
Oh rad! Nice work! |
@shama it's still shotty, but if we can work this down, I think we can have live dom components =D... Checkout SkateJS (https://github.com/skatejs/skatejs) and https://w3c.github.io/webcomponents/spec/custom/ -- for life cycle It looks like even web components will use the mutation observer. Right now we are using the mutation observer on the whole But I agree, we can create a system where the ID is assigned upon the initial render. So perhaps the onmutation method sets a node.dataset.id. Then it gets tracked and returned throughout the dom. If you want to get the element back and the ID, you just use the node returned by const Component = function() {
let id, open
function onload (node) {
id = node.dataset.yoid; // =D
}
function onunload (node) {}
function onupdate (node) {}
function toggle () {
open = !open;
yo.update(components[id], render()); // =D =D
}
function render () {
return yo`<div><button onclick=${toggle}>Toggle</button> My component</div>`
}
return connect(render(), onload, onunload, onupdate);
} |
@shama here is what I came up with. A mutation observer design with some helpers:
var components = {} // setup components cache
function walkChildren (n, v) { // walk children function
v(n); for (var i = 0; i < n.childNodes.length; i++) walkChildren(n.childNodes[i], v)
}
if (window && window.MutationObserver) { // if mutation observer
var __o = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) { // sift through mutations
var m = mutations[i], x
if(m.target.dataset && m.target.dataset.yoid) { // if mutation target is a yo component
components[m.target.dataset.yoid].node = m.target // add node to components cache
components[m.target.dataset.yoid].mutated(m.target) // fire mutation callback
}
for (x = 0; x < m.addedNodes.length; x++) { // check for components added
walkChildren(m.addedNodes[x], function(n){ // go through added nodes in mutation
if(!n.dataset) return; if(!n.dataset.yoid) return // if yo component
components[n.dataset.yoid].node = n // add node to components cache
components[n.dataset.yoid].added(n) // fire added callback
});
}
for (x = 0; x < m.removedNodes.length; x++) { // check for components removed
walkChildren(m.removedNodes[x], function(n){ // go through removed nodes in mutation
if(!n.dataset) return; if(!n.dataset.yoid) return // if yo component
components[n.dataset.yoid].removed(n) // fire removed callback
});
}
}
})
__o.observe(document.body, {childList: true, subtree: true}) // observe the body dom
}
// ./connect.js -- this will connect the element gen. yoid for component, and add to components cache
function connect (el, l, u, m) {
el.dataset.yoid = "id" + parseInt(Math.random() * 10000000)
components[el.dataset.yoid] = {
node: el, added: (l || function() {}),
mutated: (m || function() {}), removed: (u || function() {})
}
return el
}
// ./retrieve.js -- retrieve a component from the components cache with their yoid
function retrieve (yoid) {
return components[yoid];
}
// ./update.js -- a yo.update helper that moves the yoid from one el to another
function update (el, newEl, opts) {
el = typeof el === "string" && retrieve(el).node || el
newEl.dataset.yoid = el.dataset.yoid
yo.update(el, newEl, opts)
} const Component = function(_yield) {
var id, open
function onload (node) {
id = node.dataset.yoid
}
function onupdate (node) {
id = node.dataset.yoid
}
function onunload (node) {
}
function toggle () {
open = !open
update(id, render(_yield))
}
function render (_yield) {
return yo`<div><button onclick=${toggle}>Toggle</button> ${open && "Open!" || "Closed!"} ${_yield}</div>`
}
return connect(render(_yield), onload, onunload, onupdate)
} |
brilliant! 👍 |
MutationObserver support looks great http://caniuse.com/#search=MutationObserver
|
@shama @maxogden packaged... has no tests... we need to do a lot of tests... https://github.com/silentcicero/throw-down - so dont spread the word ;) |
Github is here: |
Can we clarify the pattern / use case? It's not clear to me how data is passed from a parent component to a child. Say we take the example component and want to pass it a random number. var yo = require('yo-yo')
var color = require('randomcolor')
var Component = require('./component')
function renderParent(data) {
data = data || { number: 0, color: 'black' }
return yo`
<div id="parent">
<span style="color: ${data.color};">
${Component(data.number)}
</span>
</div>
`
}
var el = renderParent()
document.body.appendChild(el)
setInterval(function() {
yo.update(el, renderParent({
number: Math.random(),
color: color()
}))
}, 1000) P.S. great job everyone |
@nichoth so you can pass it with the component constructor or state management (redux/storeEmitter) - higher up in the context. Essentially, you can do it what ever way you want. I like the component to be opts (options) first, _yield/children (DOM children/components) second in the component function/constructor, kind of like hyperx. See my So I would do something like: const MyComponent = function(opts, _yield){
opts = typeof opts === "undefined" && {} || opts // catch undefined options
_yield = typeof _yield === "undefined" && "" || _yield // catch undefined yield/children
function render (_yield) {
return yo`<div>Random Number ${opts.randomNumber} - ${_yield}</div>`
}
return render(_yield);
}
const MyParentComponent = function(opts, _yield){
opts = typeof opts === "undefined" && {} || opts // catch undefined options
_yield = typeof _yield === "undefined" && "" || _yield // catch undefined yield/children
function render (_yield) {
return yo`<div>My parent component ${Component({randomNumber: Math.random()}, "Some Yield or Child content")}</div>`
}
return render(_yield);
}
document.body.appendChild(MyParentComponent()); So you pass the random numbers in the |
And would the child's internal state be kept (via throw-down) after the parent is re-rendered? Or is this not the goal of module? Thanks for answering questions. I will try to look at it more tomorrow. |
@nichoth the child can be kept in internal state, but from what I see so far, if you're modules union together parent(child(child2(child3())) the child _yields actually keep their composure. So you can use it intuitively like you would use HTML/React elements. But I think that's depending on how/when/where you morph. More tests are needed. As for throw-down. That is really just going to help you keep track of an element from within a component. So when you use yo.update(myComponentElement,... ) within a component on the components main element, yo.update actually selects/updates the right element, and not one that either doesn't exist or has been morphed elsewhere. const yo = require("yo-yo")
const connect = require("throw-down/connect")
const update = require("throw-down/update")(yo.update)
const Component = function(_yield) {
var el, open
function track (node) {
el = node
}
function toggle () {
open = !open
update(el, render(_yield))
}
function render (_yield) {
return yo`<div><button onclick=${toggle}>Toggle</button> ${open && "Open!" || "Closed!"} ${_yield}</div>`
}
return connect(render(_yield), track, null, track)
}
document.body.appendChild(Component()); All |
Great thank you |
@nichoth just made a critical update to |
So I've been experimenting a lot with life cycles and yoyo components. So far the most reliable component design I've found is to use randomly generated or pre-assigned ID's, querySelection and the MutationObserver.
Here is an example component:
My reusable MutationObserver which tracks when the component loads, unloads and updates:
-- 335 bytes min+gz
I believe this to be the most reliable way to make stand alone components that update themselves.
Central Reason: if your component internally selects a node and not an ID, the dom can be morphed from above, altering the selected node, without changing the original node target in the component. The component then becomes useless, as its event methods are targeting a dom node that no longer exists. I ran into this problem constantly when I was using
yo.update
on higher level components, which didn't update the node targets of the morphed lower level components.Using ID's and querySelecting seems to be the most reliable, light-weight and simple solution to this problem. This MutationObserver pattern is similar to @shama's
on-load
, but it uses querySelecting and not an actual dom node.I didn't find the
morphdom-hooks
to be reliable either, as they would look for a dom node target that would sometimes not exist. This is because higher level hooks dont walk the dom lower and update component targets below.Diablo
is a system that does this. However, it follows the react pattern fairly heavily, and I couldn't get it running without issues. I'm not huge on a react knock off either for my components.Would love to get thoughts on this design and a name for my querySelector mutationobserver =D?
The text was updated successfully, but these errors were encountered: