Giter VIP home page Giter VIP logo

etch's Introduction

Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our official announcement

Logo

CI

Etch is a library for writing HTML-based user interface components that provides the convenience of a virtual DOM, while at the same time striving to be minimal, interoperable, and explicit. Etch can be used anywhere, but it was specifically designed with Atom packages and Electron applications in mind.

Overview

Etch components are ordinary JavaScript objects that conform to a minimal interface. Instead of inheriting from a superclass or building your component with a factory method, you access Etch's functionality by passing your component to Etch's library functions at specific points of your component's lifecycle. A typical component is structured as follows:

/** @jsx etch.dom */

const etch = require('etch')

class MyComponent {
  // Required: Define an ordinary constructor to initialize your component.
  constructor (props, children) {
    // perform custom initialization here...
    // then call `etch.initialize`:
    etch.initialize(this)
  }

  // Required: The `render` method returns a virtual DOM tree representing the
  // current state of the component. Etch will call `render` to build and update
  // the component's associated DOM element. Babel is instructed to call the
  // `etch.dom` helper in compiled JSX expressions by the `@jsx` pragma above.
  render () {
    return <div></div>
  }

  // Required: Update the component with new properties and children.
  update (props, children) {
    // perform custom update logic here...
    // then call `etch.update`, which is async and returns a promise
    return etch.update(this)
  }

  // Optional: Destroy the component. Async/await syntax is pretty but optional.
  async destroy () {
    // call etch.destroy to remove the element and destroy child components
    await etch.destroy(this)
    // then perform custom teardown logic here...
  }
}

The component defined above could be used as follows:

// build a component instance in a standard way...
let component = new MyComponent({foo: 1, bar: 2})

// use the component's associated DOM element however you wish...
document.body.appendChild(component.element)

// update the component as needed...
await component.update({bar: 2})

// destroy the component when done...
await component.destroy()

Note that using an Etch component does not require a reference to the Etch library. Etch is an implementation detail, and from the outside the component is just an ordinary object with a simple interface and an .element property. You can also take a more declarative approach by embedding Etch components directly within other Etch components, which we'll cover later in this document.

Etch Lifecycle Functions

Use Etch's three lifecycle functions to associate a component with a DOM element, update that component's DOM element when the component's state changes, and tear down the component when it is no longer needed.

etch.initialize(component)

This function associates a component object with a DOM element. Its only requirement is that the object you pass to it has a render method that returns a virtual DOM tree constructed with the etch.dom helper (Babel can be configured to compile JSX expressions to etch.dom calls). This function calls render and uses the result to build a DOM element, which it assigns to the .element property on your component object. etch.initialize also assigns any references (discussed later) to a .refs object on your component.

This function is typically called at the end of your component's constructor:

/** @jsx etch.dom */

const etch = require('etch')

class MyComponent {
  constructor (properties) {
    this.properties = properties
    etch.initialize(this)
  }

  render () {
    return <div>{this.properties.greeting} World!</div>
  }
}

let component = new MyComponent({greeting: 'Hello'})
console.log(component.element.outerHTML) // ==> <div>Hello World!</div>

etch.update(component[, replaceNode])

This function takes a component that is already associated with an .element property and updates the component's DOM element based on the current return value of the component's render method. If the return value of render specifies that the DOM element type has changed since the last render, Etch will switch out the previous DOM node for the new one unless replaceNode is false.

etch.update is asynchronous, batching multiple DOM updates together in a single animation frame for efficiency. Even if it is called repeatedly with the same component in a given event-loop tick, it will only perform a single DOM update per component on the next animation frame. That means it is safe to call etch.update whenever your component's state changes, even if you're doing so redundantly. This function returns a promise that resolves when the DOM update has completed.

etch.update should be called whenever your component's state changes in a way that affects the results of render. For a basic component, you can implement an update method that updates your component's state and then requests a DOM update via etch.update. Expanding on the example from the previous section:

/** @jsx etch.dom */

const etch = require('etch')

class MyComponent {
  constructor (properties) {
    this.properties = properties
    etch.initialize(this)
  }

  render () {
    return <div>{this.properties.greeting} World!</div>
  }

  update (newProperties) {
    if (this.properties.greeting !== newProperties.greeting) {
      this.properties.greeting = newProperties.greeting
      return etch.update(this)
    } else {
      return Promise.resolve()
    }
  }
}

// in an async function...

let component = new MyComponent({greeting: 'Hello'})
console.log(component.element.outerHTML) // ==> <div>Hello World!</div>
await component.update({greeting: 'Salutations'})
console.log(component.element.outerHTML) // ==> <div>Salutations World!</div>

There is also a synchronous variant, etch.updateSync, which performs the DOM update immediately. It doesn't skip redundant updates or batch together with other component updates, so you shouldn't really use it unless you have a clear reason.

Update Hooks

If you need to perform imperative DOM interactions in addition to the declarative updates provided by etch, you can integrate your imperative code via update hooks on the component. To ensure good performance, it's important that you segregate DOM reads and writes in the appropriate hook.

  • writeAfterUpdate If you need to write to any part of the document as a result of updating your component, you should perform these writes in an optional writeAfterUpdate method defined on your component. Be warned: If you read from the DOM inside this method, it could potentially lead to layout thrashing by interleaving your reads with DOM writes associated with other components.

  • readAfterUpdate If you need to read any part of the document as a result of updating your component, you should perform these reads in an optional readAfterUpdate method defined on your component. You should avoid writing to the DOM in these methods, because writes could interleave with reads performed in readAfterUpdate hooks defined on other components. If you need to update the DOM as a result of your reads, store state on your component and request an additional update via etch.update.

These hooks exist to support DOM reads and writes in response to Etch updating your component's element. If you want your hook to run code based on changes to the component's logical state, you can make those calls directly or via other mechanisms. For example, if you simply want to call an external API when a property on your component changes, you should move that logic into the update method.

etch.destroy(component[, removeNode])

When you no longer need a component, pass it to etch.destroy. This function will call destroy on any child components (child components are covered later in this document), and will additionally remove the component's DOM element from the document unless removeNode is false. etch.destroy is also asynchronous so that it can combine the removal of DOM elements with other DOM updates, and it returns a promise that resolves when the component destruction process has completed.

etch.destroy is typically called in an async destroy method on the component:

class MyComponent {
  // other methods omitted for brevity...

  async destroy () {
    await etch.destroy(this)

    // perform component teardown... here we just log for example purposes
    let greeting = this.properties.greeting
    console.log(`Destroyed component with greeting ${greeting}`)
  }
}

// in an async function...

let component = new MyComponent({greeting: 'Hello'})
document.body.appendChild(component.element)
assert(component.element.parentElement)
await component.destroy()
assert(!component.element.parentElement)

Component Composition

Nesting Etch Components Within Other Etch Components

Components can be nested within other components by referencing a child component's constructor in the parent component's render method, as follows:

/** @jsx etch.dom */

const etch = require('etch')

class ChildComponent {
  constructor () {
    etch.initialize(this)
  }

  render () {
    return <h2>I am a child</h2>
  }
}

class ParentComponent {
  constructor () {
    etch.initialize(this)
  }

  render () {
    return (
      <div>
        <h1>I am a parent</div>
        <ChildComponent />
      </div>
    )
  }
}

A constructor function can always take the place of a tag name in any Etch JSX expression. If the JSX expression has properties or children, these will be passed to the constructor function as the first and second argument, respectively.

/** @jsx etch.dom */

const etch = require('etch')

class ChildComponent {
  constructor (properties, children) {
    this.properties = properties
    this.children = children
    etch.initialize(this)
  }

  render () {
    return (
      <div>
        <h2>I am a {this.properties.adjective} child</h2>
        <h2>And these are *my* children:</h2>
        {this.children}
      </div>
    )
  }
}

class ParentComponent {
  constructor () {
    etch.initialize(this)
  }

  render () {
    return (
      <div>
        <h1>I am a parent</div>
        <ChildComponent adjective='good'>
          <div>Grandchild 1</div>
          <div>Grandchild 2</div>
        </ChildComponent>
      </div>
    )
  }
}

If the properties or children change during an update of the parent component, Etch calls update on the child component with the new values. Finally, if an update causes the child component to no longer appear in the DOM or the parent component itself is destroyed, Etch will call destroy on the child component if it is implemented.

Nesting Non-Etch Components Within Etch Components

Nothing about the component composition rules requires that the child component be implemented with Etch. So long as your constructor builds an object with an .element property and an update method, it can be nested within an Etch virtual DOM tree. Your component can also implement destroy if you want to perform teardown logic when it is removed from the parent component.

This feature makes it easy to mix components written in different versions of Etch or wrap components written in other technologies for integration into an Etch component. You can even just use raw DOM APIs for simple or performance-critical components and use them straightforwardly within Etch.

Keys

To keep DOM update times linear in the size of the virtual tree, Etch applies a very simple strategy when updating lists of elements. By default, if a child at a given location has the same tag name in both the previous and current virtual DOM tree, Etch proceeds to apply updates for the entire subtree.

If your virtual DOM contains a list into which you are inserting and removing elements frequently, you can associate each element in the list with a unique key property to identify it. This improves performance by allowing Etch to determine whether a given element tree should be inserted as a new DOM node, or whether it corresponds to a node that already exists that needs to be updated.

References

Etch interprets any ref property on a virtual DOM element as an instruction to wire a reference to the underlying DOM element or child component. These references are collected in a refs object that Etch assigns on your component.

class ParentComponent {
  constructor () {
    etch.initialize(this)
  }

  render () {
    return (
      <div>
        <span ref='greetingSpan'>Hello</span>
        <ChildComponent ref='childComponent' />
      </div>
    )
  }
}

let component = new ParentComponent()
component.refs.greetingSpan // This is a span DOM node
component.refs.childComponent // This is a ChildComponent instance

Note that ref properties on normal HTML elements create references to raw DOM nodes, while ref properties on child components create references to the constructed component object, which makes its DOM node available via its element property.

Handling Events

Etch supports listening to arbitrary events on DOM nodes via the special on property, which can be used to assign a hash of eventName: listenerFunction pairs:

class ComponentWithEvents {
  constructor () {
    etch.initialize(this)
  }

  render () {
    return <div on={{click: this.didClick, focus: this.didFocus}} />
  }

  didClick (event) {
    console.log(event) // ==> MouseEvent {...}
    console.log(this) // ==> ComponentWithEvents {...}
  }

  didFocus (event) {
    console.log(event) // ==> FocusEvent {...}
    console.log(this) // ==> ComponentWithEvents {...}
  }
}

As you can see, the listener function's this value is automatically bound to the parent component. You should rely on this auto-binding facility rather than using arrow functions or Function.bind to avoid complexity and extraneous closure allocations.

Assigning DOM Attributes

With the exception of SVG elements, Etch assigns properties on DOM nodes rather than HTML attributes. If you want to bypass this behavior and assign attributes instead, use the special attributes property with a nested object. For example, a and b below will yield the equivalent DOM node.

const a = <div className='foo' />
const b = <div attributes={{class: 'foo'}} />

This can be useful for custom attributes that don't map to DOM node properties.

Organizing Component State

To keep the API surface area minimal, Etch is deliberately focused only on updating the DOM, leaving management of component state to component authors.

Controlled Components

If your component's HTML is based solely on properties passed in from the outside, you just need to implement a simple update method.

class ControlledComponent {
  constructor (props) {
    this.props = props
    etch.initialize(this)
  }

  render () {
    // read from this.props here
  }

  update (props) {
    // you could avoid redundant updates by comparing this.props with props...
    this.props = props
    return etch.update(this)
  }
}

Compared to React, control is inverted. Instead of implementing shouldComponentUpdate to control whether or not the framework updates your element, you always explicitly call etch.update when an update is needed.

Stateful Components

If your render method's output is based on state managed within the component itself, call etch.update whenever this state is updated. You could store all state in a sub-object called state like React does, or you could just use instance variables.

class StatefulComponent {
  constructor () {
    this.counter = 0
    etch.initialize(this)
  }

  render () {
    return (
      <div>
        <span>{this.counter}</span>
        <button onclick={() => this.incrementCounter()}>
          Increment Counter
        </button>
      </div>
    )
  }

  incrementCounter () {
    this.counter++
    // since we updated state we use in render, call etch.update
    return etch.update(this)
  }
}

What About A Component Superclass?

To keep this library small and explicit, we're favoring composition over inheritance. Etch gives you a small set of tools for updating the DOM, and with these you can accomplish your objectives with some simple patterns. You could write a simple component superclass in your application to remove a bit of boilerplate, or even publish one on npm. For now, however, we're going to avoid taking on the complexity of such a superclass into this library. We may change our mind in the future.

Customizing The Scheduler

Etch exports a setScheduler method that allows you to override the scheduler it uses to coordinate DOM writes. When using Etch inside a larger application, it may be important to coordinate Etch's DOM interactions with other libraries to avoid synchronous reflows.

For example, when using Etch in Atom, you should set the scheduler as follows:

etch.setScheduler(atom.views)

Read comments in the scheduler assignment and default scheduler source code for more information on implementing your own scheduler.

Performance

The github.com/krausest/js-framework-benchmark runs various benchmarks using different frameworks. It should give you an idea how etch performs compared to other frameworks.

Checkout the benchmarks here.

Feature Requests

Etch aims to stay small and focused. If you have a feature idea, consider implementing it as a library that either wraps Etch or, even better, that can be used in concert with it. If it's impossible to implement your feature outside of Etch, we can discuss adding a hook that makes your feature possible.

etch's People

Contributors

aminya avatar as-cii avatar binarymuse avatar damieng avatar darangi avatar dy avatar glencfl avatar joefitzgerald avatar joshaber avatar kuychaco avatar leroix avatar lierdakil avatar lloiser avatar macklinu avatar matthewwithanm avatar maxbrunsfeld avatar mehcode avatar monster860 avatar sadick254 avatar simurai avatar smashwilson avatar thomasjo avatar uzitech avatar zaygraveyard avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

etch's Issues

CI not passing (xvfb config needs to be updated)

Prerequisites

Description

Travis CI runs fail with this error:

The command "export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start" failed and exited with 127 during .

This is because the CI config is setting up xvfb in a way that only works on the Trusty Travis CI image.

before_script:
  - export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start

Travis CI docs for setting up xvfb: https://docs.travis-ci.com/user/gui-and-headless-browsers#using-xvfb-to-run-tests-that-require-a-gui

The config should set dist: trusty to pin CI runs to a Trusty environment, or even better, use the config that is compatible with Travis CI's Xenial and newer images:

services:
  - xvfb

Steps to Reproduce

  1. Run Travis CI for this repository

Expected behavior:

With nothing having changed on master branch, this repo's CI should pass today just as much as it did when it was last run, in September 2018.

Actual behavior:

CI fails, due to updated default Travis CI environment (Xenial) requiring updated xvfb config.

Reproduces how often:

100% of the time.

Versions

master branch of this repository is affected.

Additional Information

I will post a PR to fix this. I prefer the "use Xenial/newer with proper configs" solution, rather than the "pin CI to Trusty" solution, personally.

Wasn't able to properly render component with atom-text-editor inside

Hi.

I like etch, thanks for this great lib. Recently I tried to create component, but didn't know how to pass attributes to atom-text-element in render method. I wrote a test to reproduce issue:

'use babel'
/** @jsx etch.dom */

import etch from 'etch'

describe('atom-text-editor and it\'s arguments', () => {
  it('should respect mini', () => {
    class TestComponent {
      constructor() {
        etch.createElement(this)
      }
      render() {
        return (
          <div className='some-class'>
            <atom-text-editor mini></atom-text-editor>
          </div>
        )
      }
    }

    let component = new TestComponent()

    expect(component.element.innerHTML)
      .toEqual('<atom-text-editor mini class="editor mini" tab-index="-1"></atom-text-editor>')
  })
})

Above test is failing with result:

atom-text-editor and it's arguments
  it should respect mini
    Expected '<atom-text-editor class="editor" tabindex="-1"></atom-text-editor>' 
    to equal '<atom-text-editor mini class="editor mini" tab-index="-1"></atom-text-editor>'

Did I miss something?

Consider testing with newer Electron

Summary

Update the electron devDependency in package.json, so tests and CI run against a newer version of Electron.

Motivation

I expect testing on newer Electron is good idea, to make sure this package still works in the latest Electron apps.

Describe alternatives you've considered

Keep testing against Electron v1.x?

Additional context

This package currently tests against Electron ^v1.7.11.

The latest stable Electron is v9, soon to be v10 in August 2020.

Atom is on Electron v5.0.13. Hopefully, soon to be v6.1.12.

Changing the root DOM node of a component

Since Etch components must opt-in to updates via the update method, and Etch updates each one individually via performElementUpdate, there is a situation where patching does not work as expected. Here is a test case to demonstrate:

class Component {
  constructor () {
    this.renderDiv = true
    etch.createElement(this)
  }

  render () {
    if (this.renderDiv) {
      return <div>Imma Div</div>
    } else {
      return <span>Imma Span</span>
    }
  }
}

let component = new Component()
component.renderDiv = false
etch.updateElementSync(component)

If you're anything like me, you might be surprised that component.element.outerHTML is still <div>Imma Div</div>. This actually makes perfect sense, because virtual-dom can't actually patch the div to make it a span; in fact, patch returns a new root node so that you can do whatever you need to with it.

This is particularly troublesome in Etch because each component's element is patched independently; for example, imagine a scenario where a top-level component renders a different component depending on a flag, and those components each render yet another component:

┌───────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │                          <Grandparent />                          │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│                                   │                                   │
│             Flag True             │            Flag False             │
│                                   │                                   │
│                                   │                                   │
│ ┌───────────────────────────────┐ │ ┌───────────────────────────────┐ │
│ │          <ParentA />          │ │ │          <ParentB />          │ │
│ └───────────────────────────────┘ │ └───────────────────────────────┘ │
│                 │                 │                 │                 │
│                 │                 │                 │                 │
│                 ▼                 │                 ▼                 │
│ ┌───────────────────────────────┐ │ ┌───────────────────────────────┐ │
│ │          <ChildA />           │ │ │          <ChildB />           │ │
│ └───────────────────────────────┘ │ └───────────────────────────────┘ │
│                 │                 │                 │                 │
│                 │                 │                 │                 │
│                 ▼                 │                 ▼                 │
│ ┌───────────────────────────────┐ │ ┌───────────────────────────────┐ │
│ │            <div />            │ │ │           <span />            │ │
│ └───────────────────────────────┘ │ └───────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘

In this case, it's not obvious that the root DOM node of Grandparent might change from a div to a span, since ChildA and ChildB may be far removed, or rendered only in certain conditions, or among a larger tree of other components.

@nathansobo and I discussed a few ways to handle this issue (Nathan please remind me if I forgot any):

  1. Just don't support returning a different type of root DOM node from a component's render

    This has potential advantages conceptually (e.g. a component only ever maps to a single DOM node, which will always be the same reference). A small change to performElementUpdate to detect the root node type changing could be used to implement this cleanly.

    To deal with this, I'd imagine components would either have their contents wrapped in lots of divs or else components would need to be less fine-grained than you might see in other component frameworks (e.g. React).

  2. Automatically swap out nodes in Widget#destroy if we detect the node has a parent

    In this case, we would do what I think the user probably expects most of the time. It doesn't work for the root DOM node of the root component unless the user has attached it to the DOM before it updates for the first time, but works nicely for components further down the component hierarchy. One potential downside of this technique is that the user doesn't have an opportunity to clean up e.g. event handlers or other references to DOM nodes.

    We realized that the reason this isn't an issue with React is the synthetic event system.

  3. Provide an API that is called when the root node is changed / needs to be updated

    This would solve the issue when the root DOM node for the root component changes (as discussed in option 2), and would allow users to have a chance to clean up references to the nodes if necessary. However, this introduces a fair amount of new complexity into the library, and it's unclear if it's worth it.

  4. Force the user to provide a container

    This type of issue is probably why React, et al. requires a container, but we'd like to avoid this.

Children can't be modified without instantiating new component instances?

For example, since a component constructor receives children in the second arg, then if children ever change, the only way to have it updated is constructing a new instance? This is unlike React, where the instance is kept alive, and this.props.children is modified. Could this be hint that new instances are being created when maybe there's an alternate way to avoid creating new instances (and thus reduce overhead)?

(same with props if those are only received through constructor)

TypeScript in etch

Hello, etch team,

To start off, I really appreciate Atom as a project and initiative driven by passionate contributors. Recently, I decided to dive deeper into how it works and discovered etch. I love component-based UI building approach and really like the idea of having a small library powering the entire complex UI.

This discovery is driven by an article I'm working on, which promotes TypeScript usage and is intended to give concrete recipes for TS integration into real-world projects. I think you're already guessing what I'm getting at. Alongside the article development, I'm curious and want to contribute to the project. As I said, I love the component approach and want to work toward improving my understanding of it from different sides, not only from a user perspective.

Which brings me straight to the point: how would you feel about transforming etch codebase into TypeScript? I would love to do it solving all the issues popping up along the road, but for that, for sure, I need your consent. In case you haven't envisioned such a future for the library please share your ideas on how to keep codebase complexity and developer confidentiality in check. It would be incredibly valuable to me!

And last but not least, I'm proceeding from the presumption that we're on the same page about advantages TypeScript brings to a project like etch. Since it's a foundation module for Atom, a tool that's used by millions I assume, it's better to be extremely reliable. In my opinion, a module like that would benefit most from static-typed implementation - although, the might be zero known bugs at the moment.

Thanks for your time and looking forward to getting your answer!
Kind regards,
Oleg.

Null-elements are not allowed?

There are use-cases when it is not necessary to create an element for a component, for example, when we render WebGL/regl or canvas2d layers:

<canvas id="canvas">
<Grid canvas="#canvas" type="cartesian" />
<Plot canvas="#canvas" data={data} />
<Text canvas="#canvas" text="Test Plot" />

Would that be reasonable to disable strong assertion of instance.element property? Or not forcing render to return etch virtual-dom?

Faced this issue trying to make gl-component API compatible with etch.

Issue with atom-text-editor not updating

Hi, I'm trying out Etch and so far liking it, but I converted one view to it and having trouble with an element.

If you check out https://github.com/lukemurray/data-atom/blob/master/lib/views/new-connection-dialog.js you'll see (install the package from master) that I am watching for changes on all the text editors either to build the URL when the user changes the separate values or if the type/paste a URL I break it up.

If I use {this.buildURL()} or {this.state.builtUrl} elsewhere like just in a <div> or in a normal <input> it will work. The only way I can get <atom-text-editor> to update is use this.ref.elementName.getModel().setText() but that seems backwards for Etch.

Not really sure what is happening, I can't find anything wrong with my code, although I'm new to Etch so I might be abusing it.

Any ideas?

JSX Dynamic Component Name

Hi,

Would be thankful for any hints how to render Dynamic Components. Similar to this question.

I tried different options but it doesn't work after etch.update(this).

Use case

class MyComponent {
  constructor (properties) {
    this.properties = properties
    etch.initialize(this)
  }

  renderDynComponent () {
    if (case1) {
      return <DynComponent1 />
    }
    else if (case2) {
      return <DynComponent2 />
    }
    return <DynComponentDefault />
  }

  render () {
    return <div>{this.renderDynComponent()}</div>
  }
}

Thanks!

How to move an Etch component's child?

Consider the following scenario:
I have a root component that holds some rows. I also have a button that should be a child of the latest row at all times.

On the root component creation it holds a single row which in turn holds the button, so the inital markup looks like this:

<Root>
  <Row ref="row0">
    <Button ref="btn" />
  </Row>
</Root>

Then at some point I append another Row inside Root and I want the markup to look like this:

<Root>
  <Row ref="row0"></Row>
  <Row ref="row1">
    <Button ref="btn" />  // this should point to the same DOM node as the previous Button
  </Row>
</Root>

Here's my attempt at doing this:

class Button {
  constructor() {
    etch.initialize(this);
  }

  render() {
    return (
      <button>Test</button>
    );
  }

  update() {
    return etch.update(this);
  }
}

class Row {
  constructor(props, children) {
    this.children = children;
    etch.initialize(this);
  }

  render() {
    return (
      <div>
        {this.children}
      </div>
    );
  }

  update() {
    return etch.update(this);
  }
}

export default class Root {
  constructor() {
    this.otherRows = [];
    this.rowCount = 1;
    etch.initialize(this);
  }

  render() {
    return (
      <div>
        <Row ref="row0">
          <Button ref="btn" />
        </Row>
        {this.otherRows}
      </div>
    );
  }

  addRow() {
    const lastRow = this.refs[`row${this.rowCount - 1}`];
    const button = lastRow.children.pop();
    const newRow = new Row({}, [button]);

    this.otherRows.push(newRow);
    this.update();
    this.rowCount++;
  }

  update() {
    return etch.update(this);
  }
}

And when I call the addRow method after Root's establishment, I get the following markup:

<div> // This is the Root
  <div>  // This is a Row
    <button />
  </div>
  <undefined>
    <button />
  </undefined>
</div>

And it seems like the two buttons that appear in the markup are different DOM nodes.
How do I achieve my goal?

No scheduler is ever set

The result is that any call to etch.update, or attempts to use etch.observe fails with

Cannot read property 'updateDocument' of null

which originates in element-prototype.js from the update function

update: {
  value: function () {
    if (!this._updateRequested) {
      this._updateRequested = true
      getScheduler().updateDocument(() => {
        this._updateRequested = false
        this.updateSync()
      })
    }
  }
}

Since setScheduler is never called, getScheduler obviously returns null. Similar failure occurs when attempting to use etch.observe.

The tests are passing because there's a TestScheduler wired up as part of the test harness setup.

Can't render custom elements

It looks like virtual-dom doesn't play nicely with custom elements 😞

Steps to Reproduce

  1. Create a custom element (e.g. atom-text-editor)
  2. Try to render that custom element in JSX
  3. etch will throw an error:
Error: Failed to execute 'createElement' on 'Document': The tag name provided ('[') is not a valid name.
    at Error (native)
    at createElement (/Users/machistequintana/github/command-palette/node_modules/virtual-dom/vdom/create-element.js:30:13)
    at createElement (/Users/machistequintana/github/command-palette/node_modules/virtual-dom/vdom/create-element.js:39:25)
    at Object.initialize (/Users/machistequintana/github/command-palette/node_modules/etch/dist/component-helpers.js:68:64)

CoffeeScript support

I was playing around with this again and I thought that what could be cool for CoffeeScript is something like this:

# Private: Registers the custom element with the DOM.
registerElement: ->
  etch.defineElement 'div',
    render: (html) ->
      html.div {className: 'soft-wrap-status inline-block'},
        html.a {href: '#'}, 'Wrap'

    initialize: ->
      this

The html object would just need to be an object with a bunch of helpers on it that look like this (except in ES6):

div = (properties, children...) ->
  dom 'div', properties, children

a = (properties, children...) ->
  dom 'a', properties, children

I've tested this strategy with just using hard coded helpers (not passing parameters through render) and it seems to work.

Get tests running on Travis

Travis doesn't seem to want to run electron-mocha correctly; even with failing tests checked in, the command produces no output and Travis says that the script exited with code 0.

Children unexpectedly removed from dom

I'm looking through patch.js trying to find the problem, but to be honest it's a wall of text and makes my eyes hurt. Would somebody more familiar with the code take a look.

My Component represents a <ul>, it ignores any children passed to the constructor. It builds the <li> array by parsing a file. Normally I would use etch.dom, but I want to provide the owner a way of modifying text before it is rendered. (In particular attaching click handlers to strings representing local system paths). These <li> items are created with the raw document.createElement and stored in the component. They are returned from my render method wrapped in a constructor function.

The problem I am seeing is, upon creation everything is rendered fine. As soon as an update is triggered further up the chain, all my list items are removed from the dom, even though my own render method returns exactly the same result. Note, the update method of my component is never called.

Following the debug, I see my element being added at line 102 then removed by calling line 122.

A workaround is to return cloned elements from my render method, but that causes problems for any event listeners.

input cursor jumps to the end

The cursor will jump to the end of an input when changing the value in the middle.

This started happening in v0.9.0

Uncaught TypeError: Invalid TextEditor parameter: 'ref'

[Enter steps to reproduce:]

  1. Switch to Etch 0.9.0 (<0.9.0 is OK)
  2. Use the next code
import { TextEditor } from 'atom';

...
render() {
    return <TextEditor ref='filterEditor' mini={ true } />
}
...

etch: 0.9.0
Atom: 1.14.3 x64
Electron: 1.3.13
OS: Mac OS X 10.12.3

Stack Trace

Uncaught TypeError: Invalid TextEditor parameter: 'ref'

At /Applications/Atom.app/Contents/Resources/app.asar/src/text-editor.js:437

TypeError: Invalid TextEditor parameter: 'ref'
    at TextEditor.module.exports.TextEditor.update (/app.asar/src/text-editor.js:437:19)
    at updateComponent (xxxx/node_modules/etch/lib/patch.js:49:13)
    at patch (xxxx/node_modules/etch/lib/patch.js:13:19)
    at updateChildren (xxxx/node_modules/etch/lib/patch.js:72:7)
    at patch (xxxx/node_modules/etch/lib/patch.js:15:9)
    at updateSync (xxxx/node_modules/etch/lib/component-helpers.js:108:20)
    at Object.update (xxxx/node_modules/etch/lib/component-helpers.js:62:5)

selected item in select

I'm using Etch to build a plugin and I have a select. I've try to use "React style" of having <select value={this.state.myval} but it does not seem to work.

I've end up running the following line after etch.initialize(this):
this.refs.myselect.value = this.state.myval

Do you have a recommended way of selecting an item in a select.

Add some form of reference/alias to root tag name

Right now the element's root tag is duplicated in both the registration and the render method—e.g.

const TaskList = etch.defineElement('div', {
  // Define the element's content via a `render` method
  render () {
    return (
      <div className='task-list'>
        <h1>Tasks:</h1>
        <ol>{
          this.tasks.map(task =>
            <li className='task' key={task.id}>
              <input type='checkbox' checked={task.completed}>
              task.description
            </li>
          )
        }</ol>
      </div>
    )
  },
// ...

It would be awesome if this could be avoided by introducing some type of reference, alias or some such. Perhaps something like

const TaskList = etch.defineElement('div', {
  // Define the element's content via a `render` method
  render () {
    return (
      <root className='task-list'>
        <h1>Tasks:</h1>
// ...

“React is not defined” while using etch in atom?

Although React is not a dependency of etch, it says "React is not defined".

my plugin code is as follows :

> 'use babel';

import NewPluginView from './new-plugin-view';
import { CompositeDisposable } from 'atom';

export default {

  newPluginView: null,
  modalPanel: null,
  subscriptions: null,

// document.body.appendChild(component.element)

  activate(state) {
    this.newPluginView = new NewPluginView(state.newPluginViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.newPluginView.getElement(),
      visible: false
    });

    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'new-plugin:toggle': () => this.toggle()
    }));
  },

  deactivate() {
    this.modalPanel.destroy();
    this.subscriptions.dispose();
    this.newPluginView.destroy();
  },

  serialize() {
    return {
      newPluginViewState: this.newPluginView.serialize()
    };
  },

  toggle() {
    console.log('NewPlugin was toggled!');
    return (
      this.modalPanel.isVisible() ?
      this.modalPanel.hide() :
      this.modalPanel.show()
    );
  }

};

and My view code is as follows :

'use babel';

import etch from 'etch';

export default class NewPluginView {

  constructor(serializedState) {
    // Create root element
    // this.element = document.createElement('div');
    // this.element.classList.add('new-plugin');
    //
    // // Create message element
    // const message = document.createElement('div');
    // message.textContent = 'The NewPlugin package is Alive! It\'s ALIVE!';
    // message.classList.add('message');
    // this.element.appendChild(message);
    // console.log('this package');
    etch.initialize(this);
  }


  update(props, children) {
    return etch.update(this);
  }
  // Returns an object that can be retrieved when package is activated
  serialize() {}

  // Tear down any state and detach
  destroy() {
    //this.element.remove();
  }

  getElement() {
    return this.element;
  }

  render() {
    return (
     <div></div>
    );
  }
}

Can someone help me?

Does not update children components?

So let us imagine I have some code like this:

render () {
    return (
      <ide-haskell-panel-items class='native-key-bindings' tabIndex='-1'>
        {
          this.items.map(
            (item) => <OutputPanelItem model={item} />
          )
        }
      </ide-haskell-panel-items>
    );
  }

This works fine the first time around. Then I completely change all this.items, and run this.update. Children are only added/removed, but contents doesn't change. I would assume actual contents of children components isn't checked when computing a diff.

So... I would guess this is a "feature", since it avoids creating components unnecessarily. But in this use-case, this leads to problems.

I can work around that with something like this:

async updateItems(items) {
  this.items = []
  await this.update()
  this.items = items
  this.update()
}

but that's kinda awkward and leads to two updates instead of one.

So... any advice?

Some SVG attributes are stripped before rendering

Steps to reproduce:

  1. Try sticking this JSX in a component's render method:

    <svg className='about-logo' width='330px' height='68px' viewBox='0 0 330 68'>
      <g stroke='none' strokeWidth='1' fill='none' fillRule='evenodd'>
        <g transform='translate(2.000000, 1.000000)'>
          <g transform='translate(96.000000, 8.000000)' fill='currentColor'>
            <path d='M185.498,3.399 C185.498,2.417 186.34,1.573 187.324,1.573 L187.674,1.573 C188.447,1.573 189.01,1.995 189.5,2.628 L208.676,30.862 L227.852,2.628 C228.272,1.995 228.905,1.573 229.676,1.573 L230.028,1.573 C231.01,1.573 231.854,2.417 231.854,3.399 L231.854,49.403 C231.854,50.387 231.01,51.231 230.028,51.231 C229.044,51.231 228.202,50.387 228.202,49.403 L228.202,8.246 L210.151,34.515 C209.729,35.148 209.237,35.428 208.606,35.428 C207.973,35.428 207.481,35.148 207.061,34.515 L189.01,8.246 L189.01,49.475 C189.01,50.457 188.237,51.231 187.254,51.231 C186.27,51.231 185.498,50.458 185.498,49.475 L185.498,3.399 L185.498,3.399 Z'></path>
            <path d='M113.086,26.507 L113.086,26.367 C113.086,12.952 122.99,0.941 137.881,0.941 C152.77,0.941 162.533,12.811 162.533,26.225 L162.533,26.367 C162.533,39.782 152.629,51.792 137.74,51.792 C122.85,51.792 113.086,39.923 113.086,26.507 M158.74,26.507 L158.74,26.367 C158.74,14.216 149.89,4.242 137.74,4.242 C125.588,4.242 116.879,14.075 116.879,26.225 L116.879,26.367 C116.879,38.518 125.729,48.491 137.881,48.491 C150.031,48.491 158.74,38.658 158.74,26.507'></path>
            <path d='M76.705,5.155 L60.972,5.155 C60.06,5.155 59.287,4.384 59.287,3.469 C59.287,2.556 60.059,1.783 60.972,1.783 L96.092,1.783 C97.004,1.783 97.778,2.555 97.778,3.469 C97.778,4.383 97.005,5.155 96.092,5.155 L80.358,5.155 L80.358,49.405 C80.358,50.387 79.516,51.231 78.532,51.231 C77.55,51.231 76.706,50.387 76.706,49.405 L76.706,5.155 L76.705,5.155 Z'></path>
            <path d='M0.291,48.562 L21.291,3.05 C21.783,1.995 22.485,1.292 23.75,1.292 L23.891,1.292 C25.155,1.292 25.858,1.995 26.348,3.05 L47.279,48.421 C47.49,48.843 47.56,49.194 47.56,49.546 C47.56,50.458 46.788,51.231 45.803,51.231 C44.961,51.231 44.329,50.599 43.978,49.826 L38.219,37.183 L9.21,37.183 L3.45,49.897 C3.099,50.739 2.538,51.231 1.694,51.231 C0.781,51.231 0.008,50.529 0.008,49.685 C0.009,49.404 0.08,48.983 0.291,48.562 L0.291,48.562 Z M36.673,33.882 L23.749,5.437 L10.755,33.882 L36.673,33.882 L36.673,33.882 Z'></path>
          </g>
          <g>
            <path d='M40.363,32.075 C40.874,34.44 39.371,36.77 37.006,37.282 C34.641,37.793 32.311,36.29 31.799,33.925 C31.289,31.56 32.791,29.23 35.156,28.718 C37.521,28.207 39.851,29.71 40.363,32.075' fill='currentColor'></path>
            <path d='M48.578,28.615 C56.851,45.587 58.558,61.581 52.288,64.778 C45.822,68.076 33.326,56.521 24.375,38.969 C15.424,21.418 13.409,4.518 19.874,1.221 C22.689,-0.216 26.648,1.166 30.959,4.629' stroke='currentColor' strokeWidth='3.08' strokeLinecap='round'></path>
            <path d='M7.64,39.45 C2.806,36.94 -0.009,33.915 0.154,30.79 C0.531,23.542 16.787,18.497 36.462,19.52 C56.137,20.544 71.781,27.249 71.404,34.497 C71.241,37.622 68.127,40.338 63.06,42.333' stroke='currentColor' strokeWidth='3.08' strokeLinecap='round'></path>
            <path d='M28.828,59.354 C23.545,63.168 18.843,64.561 15.902,62.653 C9.814,58.702 13.572,42.102 24.296,25.575 C35.02,9.048 48.649,-1.149 54.736,2.803 C57.566,4.639 58.269,9.208 57.133,15.232' stroke='currentColor' strokeWidth='3.08' strokeLinecap='round'></path>
          </g>
        </g>
      </g>
    </svg>
  2. The fillRule and strokeWidth attributes (and others too, probably) are stripped from the rendered DOM element.

refs are being assigned to the wrong component

Looking at the following example:

class MainComponent {
  //...
  render () {
    class Component {
      constructor (props, children) {
        this.children = children
        this.props = props

        etch.initialize(this)
      }

      update () {
        return Promise.resolve()
      }

      render () {
        return (<div id={this.props.id}>{ this.children }</div>)
      }
    }

    return (
      <Component id='outer' ref="outer">
        <Component id='middle' ref="middle">
          <div id="inner" ref="inner" />
        </Component>
      </Component>
    )
  }
}

I would expect all ref's to be applied to the MainComponent instance. Instead, I am seeing MainComponent with the nested object refs.outer.refs.middle.refs.inner. Surely this is wrong. If the Component class happened to also ref one of its element, with the name 'middle' or 'inner', all hell would break loose.

Virtual DOM gets updated but actual element doesnt

/** @jsx etch.dom */

const etch = require('etch')

class MyComponent {
  constructor (properties) {
    this.properties = properties
    etch.initialize(this)
  }

  render () {
    return <div>{this.properties.greeting} World!</div>
  }

  update (newProperties) {
    if (this.properties.greeting !== newProperties.greeting) {
      this.properties.greeting = newProperties.greeting
      return etch.update(this)
    } else {
      return Promise.resolve()
    }
  }
}

// in an async function...

let component = new MyComponent({greeting: 'Hello'})
console.log(component.element.outerHTML) // ==> <div>Hello World!</div>
await component.update({greeting: 'Salutations'})
console.log(component.element.outerHTML)

Im using this exact same sample and its not working, the virtualNode object gets update, but the actual text of the element isnt!

Allow Render To Include Generated HTML

let createMarkup = () => { return {__html: '<span style="text-decoration: blink;">Because who uses the blink tag?</span>'} }
return (
  <div
    className='content'
    dangerouslySetInnerHTML={createMarkup()}
  />
)

Renders <div class="content" />.

let markup = '<span style="text-decoration: blink;">Because who uses the blink tag?</span>'
return (
  <div className='content'>{markup}</div>
)

Renders <div class="panel-body padded">&lt;span style="text-decoration: blink;"&gt;Because who uses the blink tag?&lt;/span&gt;</div>

If one wishes to generate dynamic HTML content and emit it in an etch view, they cannot. Would there be a way to allow this?

Component lifecycle and updates

Suppose I have ComponentA and ComponentB.

class ComponentA {
  render () {
    return <ComponentB/>
  }
}

class ComponentB {
  render () {
    // some stuff
  }

  update (props, children) {
    // I never get called :(
  }
}

ComponentB is some stateful mess such that it needs to implement update. Ideally, ComponentA shouldn't have to know about this. It should be entirely encapsulated in ComponentB.

But ComponentBs update method is never called unless ComponentA also implements update. In this way, implementation details are forced to trickle up the component chain.

I haven't dug into this enough to know exactly why.

How to pass attribute for element without value?

Hi,

I tried different options but without success. An Atom's Text Editor has mini attribute. What is a correct syntax to set it? I use

<atom-text-editor mini={true}>Hello</atom-text-editor>

Thanks.

onChange not working

<form onSubmit={this.handleSubmit} className="input-fields"> <b>Name</b>: <input type="text" value={ this.state.configKey } className="input-text native-key-bindings" onChange={ this.state.configKey }/> <b>Value</b>: <input type="text" value={ this.state.configVal } className="input-text native-key-bindings" onChange={ this.state.configVal }/><br/> <input type="submit" value="Save Project Moov Config" className="btn mv-consoleBtn" /> </form>

I'm trying to grab these input values for a form im submitting, but I'm not getting the values. The onChange doesnt even register a console.log, it only fires on page load

lifecycle events

I'm experimenting with using etch for an Atom plugin.
I'm trying to use a bootstrap plugin (http://www.bootstrap-switch.org/examples.html) which turns checkboxes into nifty toggle buttons.

Like most of these types of plugins, you create the elements and then call init to activate the plugin.
In render I'm iterating through a list to add checkboxes but I'm having an issue with calling the init on the plugin.

A snippet showing how I am currently doing things..

export default class AtomPackageTestView {
    constructor(transports) {
        this.providers = transports;
        etch.createElement(this);
        // jquery and bootstrap helpers/plugins
        jQuery = window.jQuery = require("jquery");
        require("C:/Users/dashe/.atom/packages/atom-debug-service/bower_components/bootstrap/dist/js/bootstrap.js");
        require("C:/Users/dashe/.atom/packages/atom-debug-service/assets/bootstrap-switch.js");
    }
    render() {
        return ( <div className = 'atom-debug-service padded native-key-bindings'
            tabIndex = '-1' > {
                this.renderHeading()
            } {
                this.renderLogProviders()
            } </div>
        )
    }

At some point I need to call jQuery(".toggle-button").bootstrapSwitch(); but I'm unclear on how to achieve this with etch without some kind of lifecycle callback once render has completed or using yet another external plugin that taps into the dom mutation events. I don't really want to resort to nextTick..

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.