intercellular / cell

A self-driving web app framework
https://www.celljs.org
MIT License
1.5k stars 94 forks source link

Allow explicit instantiation of cells #12

Closed Caffeinix closed 7 years ago

Caffeinix commented 7 years ago

According to the official documentation, the canonical way to instantiate a cell is like this:

var x = {
  $cell: true,
  $type: 'div',  // or some other element type
  // ...
};

This automatically appends the cell to the end of the body tag (unless $type is 'body', in which case it gets injected instead). But this is very limiting: the claim that you can reuse cells by simply pasting them into existing apps is weakened by the fact that you have no real control over where those cells end up in your document without using an iframe.

Of course, as you are probably about to point out, there is already an undocumented solution to this problem:

var x = {
  $type: 'div',
  // ...
};

window.addEventListener('load', function() {
  document.querySelector('#myContainerElement').$build(x);
});

This works well, although it's a little unfortunate that we have to wait for the load event because the $build function hasn't been added to the existing DOM elements yet. This feature is powerful and useful, and I think it should be (at the very least) officially documented and supported.

It would be even nicer if we didn't have to wait for the load event. Since we've already loaded the script by the time we get here, perhaps it would be better to put the $build function on some existing object so we can access it immediately:

document.$build(document.querySelector('#myContainerElement'), x);

It might also be nice to add a $parent key on the genotype that could be used to specify a (static) parent for the element:

<body>
<div id="myContainerElement"></div>
<script src="https://www.celljs.org/cell.js"></script>
<script>
var x = {
  $cell: true,
  $type: 'div',
  $parent: document.querySelector('#myContainerElement'),
  // ...
};
</script>
</body>

However, I hope you keep the imperative version as well, because it allows developers to avoid polluting the global namespace. For example:


// This does not work:
const x = {
  $cell: true,
  $type: 'div',
  // ...
};

// But this does:
const y = {
  $type: 'div',
  // ...
};

window.addEventListener('load', () => {
  document.querySelector('#myContainerElement').$build(y);
});
gliechtenstein commented 7 years ago

Thank you for the idea! I took some time to think about this.

First of all just in case you missed it, Cell does have purely declarative ways of injecting elements into whichever position you want:

Case 1. Creating an entire DOM tree with Cell

In this case what you should be doing is write a single cell object instead of multiple. For example, instead of:

var heading = {
  $cell: true,
  $type: "h1",
  $text: "Header"
}
var paragraph = {
  $cell: true,
  $type: "p",
  $text: "This is a paragraph"
}

You should be writing:

var body = {
  $cell: true,
  $type: "body",
  $components: [{
    $type: "h1",
    $text: "Header"
  }, {
    $type: "p",
    $text: "This is a paragraph"
  }]
}

Or if you want to componentize each:

var heading = {
  $type: "h1",
  $text: "Header"
}
var paragraph = {
  $type: "p",
  $text: "This is a paragraph"
}
var body = {
  $cell: true,
  $type: "body",
  $components: [heading, paragraph]
}

Case 2. Creating a component with Cell and injecting into an existing DOM tree

In this case you can use id to auto-inject cell elements into anywhere. You just need to have an element with the same ID already on the HTML markup. Here's more on that: https://github.com/intercellular/tutorial#e-injecting-a-cell-into-an-exsiting-dom-tree


That said, coming back to the topic of exposing the $build API I would love to hear your additional thoughts after having read my comments above.

I do think it would be helpful to document this, but at the same time I think it would be the best if everything could be taken care of in purely declarative manner.

I think the methods above should cover all the cases that we don't need an explicit instantiation but please let me know if I'm missing something, or if there are other useful cases for explicit instantiation. I would love to improve it somehow. Thank you!

Caffeinix commented 7 years ago

Thanks for your quick reply! You are absolutely right, I hadn't noticed the ability to inject into an existing DOM tree. I agree that fixes the vast majority of the problem (and it's arguably more elegant than a $parent attribute, too). Given that, it makes sense to prefer that approach to the imperative $build API in most cases.

The one thing I would still like to find a solution for is the fact that declarative cells must not be lexically scoped -- only var or a global declaration will do, and neither const nor let will work. The $build API does solve that problem, but I can imagine other solutions too, such as eschewing a variable declaration entirely and having a Cell function instead:

Cell({
  $type: 'h1',
  $text: 'Hello, world!'
});

Alternatively, Cell could return the genotype which could then be assigned to a lexically scoped variable:

const heading = Cell({
  $type: 'h1',
  $text: 'Hello, world!'
});

That still looks reasonably declarative to me. :)

gliechtenstein commented 7 years ago

@Caffeinix you are totally right that let and const variables don't get detected in the global scope.

My first instinct to your comment was to ask "when would we need lexical scoping to declare cell variables though?". It's probably because in most cases I would only use a single Cell variable in the global scope to build an entire DOM tree, in which case there's no need for lexical scoping to begin with.

But I do want to understand if there are cases where using let or const makes sense. Could you share some scenarios where using let or const instead of var makes more sense? Would appreciate your input. Thank you!

Caffeinix commented 7 years ago

My concern is mainly about namespace collisions. For example:

var Element = { // Oops! I just overwrote the constructor for the built-in Element type!
  $cell: true,
  $type: 'h1'
}

Bugs like that are subtle, and will tend to come back to bite you long after you've forgotten what you named that variable.

As you say, there should only ever be one global variable in the app, representing the root element, so maybe this isn't the most horrible thing. But it does feel like a spooky side effect in an otherwise very functional approach. (I also wonder about performance -- do you go through every single property of window looking for a $cell attribute?)

I think a Cell() function or similar would be a safer and more explicit way of achieving the same effect, and if you decide not to implement it I'll probably use the $build API out of an abundance of paranoia, but I wouldn't call it an existential design issue. :)

dalerka commented 7 years ago

BTW, imagine site X is built with Cell.js (and has their own $cell root), site Y is built without Cell.js. Suppose I make a self-contained widget (e.g. showing tweets) using Cell.js which both sites X and Y want to embed/inject on their page. Is it advised to pack the widget in an iframe for this case? can there be any conflicts with site X?

ghost commented 7 years ago

One thing to consider is the current injection approach only works if the full DOM is parsed at load time. Having an explicit mount/$build function would be useful for starting an app within the context of a legacy application. For example I'm thinking cell would be good for something I'm doing inside of a legacy Angular application but obviously rewrite isn't in the cards. But of course the page I would want to mount on doesn't exist yet.

This is one of the big value propositions that Elm has, and potentially for Cell, is that you can introduce it incrementally as you go. I see a real value in being able to have something like cell which is more low level with some good abstractions which skirt the complexities of dealing with runtime DOM binding in frameworks like React/Angular. For me it's needing to write something more complex than what angular's template dsl gives me or adding charting libraries.

ghost commented 7 years ago

Random question to tack on. I was stepping through the $node.Dirty and diff stuff in the chrome debugger and it appears that when it comes down to it you're serializing and diffing strings. Maybe I'm reading this wrong because I see you have the LCS implementation being used though I'm not too familiar with it. I would think this wouldn't work with unordered collections like Objects, Maps, Sets.

Tangentially, I wonder what performance gains could be had using immutable datastructures with fast equality checks.

gliechtenstein commented 7 years ago

Hey guys, I have a draft for addressing this issue. Please check it out and would appreciate feedback https://github.com/intercellular/cell/issues/125

gliechtenstein commented 7 years ago

Resolving this with this release: https://github.com/intercellular/cell/releases/tag/v1.1.0 👍