Giter VIP home page Giter VIP logo

ember-render-modifiers's Introduction

@ember/render-modifiers

Provides element modifiers that can be used to hook into specific portions of the rendering lifecycle.

When to use these modifiers (and when not to use them)

The modifiers provided in this package are ideal for quickly migrating away from classic Ember components to Glimmer components, because they largely allow you to use the same lifecycle hook methods you've already written while attaching them to these modifiers. For example, a didInsertElement hook could be called by {{did-insert this.didInsertElement}} to ease your migration process.

However, we strongly encourage you to take this opportunity to rethink your functionality rather than use these modifiers as a crutch. In many cases, classic lifecycle hooks like didInsertElement can be rewritten as custom modifiers that internalize functionality manipulating or generating state from a DOM element. Other times, you may find that a modifier is not the right fit for that logic at all, in which case it's worth revisiting the design to find a better pattern.

Either way, we recommend using these modifiers with caution. They are very useful for quickly bridging the gap between classic components and Glimmer components, but they are still generally an anti-pattern. We recommend considering a custom modifier in most use-cases where you might want to reach for this package.

Compatibility

  • Ember.js v3.20 or above
  • Ember CLI v3.20 or above
  • Node.js v12 or above

Installation

ember install @ember/render-modifiers

Usage Examples

Example: Scrolling an element to a position

This sets the scroll position of an element, and updates it whenever the scroll position changes.

Before:

{{yield}}
export default class extends Component {
  @action
  didRender(element) {
    element.scrollTop = this.scrollPosition;
  }
}

After:

<div
  {{did-insert this.setScrollPosition @scrollPosition}}
  {{did-update this.setScrollPosition @scrollPosition}}
  class='scroll-container'
>
  {{yield}}
</div>
export default class extends Component {
  setScrollPosition(element, [scrollPosition]) {
    element.scrollTop = scrollPosition;
  }
}

Example: Adding a class to an element after render for CSS animations

This adds a CSS class to an alert element in a conditional whenever it renders to fade it in, which is a bit of an extra hoop. For CSS transitions to work, we need to append the element without the class, then add the class after it has been appended.

Before:

{{#if this.shouldShow}}
  <div class='alert'>
    {{yield}}
  </div>
{{/if}}
export default class extends Component {
  @action
  didRender(element) {
    let alert = element.querySelector('.alert');

    if (alert) {
      alert.classList.add('fade-in');
    }
  }
}

After:

{{#if this.shouldShow}}
  <div {{did-insert this.fadeIn}} class='alert'>
    {{yield}}
  </div>
{{/if}}
export default class extends Component {
  @action
  fadeIn(element) {
    element.classList.add('fade-in');
  }
}

Example: Resizing text area

One key thing to know about {{did-update}} is it will not rerun whenever the contents or attributes on the element change. For instance, {{did-update}} will not rerun when @type changes here:

<div {{did-update this.setupType}} class='{{@type}}'></div>

If {{did-update}} should rerun whenever a value changes, the value should be passed as a parameter to the modifier. For instance, a textarea which wants to resize itself to fit text whenever the text is modified could be setup like this:

<textarea {{did-update this.resizeArea @text}}>
  {{@text}}
</textarea>
export default class extends Component {
  @action
  resizeArea(element) {
    element.style.height = `${element.scrollHeight}px`;
  }
}

Example: ember-composability-tools style rendering

This is the type of rendering done by libraries like ember-leaflet, which use components to control the rendering of the library, but without any templates themselves. The underlying library for this is here. This is a simplified example of how you could accomplish this with Glimmer components and element modifiers.

Node component:

// components/node.js
export default class extends Component {
  constructor() {
    super(...arguments);
    this.children = new Set();

    this.args.parent.registerChild(this);
  }

  willDestroy() {
    super.willDestroy(...arguments);

    this.args.parent.unregisterChild(this);
  }

  registerChild(child) {
    this.children.add(child);
  }

  unregisterChild(child) {
    this.children.delete(child);
  }

  @action
  didInsertNode(element) {
    // library setup code goes here

    this.children.forEach(c => c.didInsertNode(element));
  }

  @action
  willDestroyNode(element) {
    // library teardown code goes here

    this.children.forEach(c => c.willDestroyNode(element));
  }
});
<!-- components/node.hbs -->
{{yield (component 'node' parent=this)}}

Root component:

// components/root.js
import NodeComponent from './node.js';

export default class extends NodeComponent {}
<!-- components/root.hbs -->
<div {{did-insert this.didInsertNode}} {{will-destroy this.willDestroyNode}}>
  {{yield (component 'node' parent=this)}}
</div>

Usage:

<Root as |node|>
  <node as |node|>
    <node></node>
  </node>
</Root>

Glint usage

If you are using Glint and environment-ember-loose, you can add all the modifiers to your app at once by adding

import type RenderModifiersRegistry from '@ember/render-modifiers/template-registry';

to your app's e.g. types/glint.d.ts file, and making sure your registry extends from RenderModifiersRegistry:

declare module '@glint/environment-ember-loose/registry' {
  export default interface Registry
    extends RenderModifiersRegistry {
      // ...
    }
}

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

ember-render-modifiers's People

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

Watchers

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

ember-render-modifiers's Issues

Cannot install library - Not Found

I'm getting the following error when installing this library:
error An unexpected error occurred: "https://registry.yarnpkg.com/@ember%2frender-modifiers: Not found".

will-destroy does not invoke re-rendering

This happened when I tried to write a HOC with a parent passing a function to its child, and the function gets called when the child is inserted and going to be destroyed. I made an integration test to demonstrate the issue.

import Component from '@glimmer/component';
import { click, find, render } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { action } from '@ember/object';
import hbs from 'htmlbars-inline-precompile';
import { setupRenderingTest } from 'ember-qunit';
import { tracked } from '@glimmer/tracking';

module('Integration | Modifier | will-destroy', hooks => {
  setupRenderingTest(hooks);

  test('it should invoke UI re-rendering when changing tracked properties', async function(assert) {
    this.owner.register(
      'component:parent-component',
      class extends Component {
        @tracked text = '';

        get message() {
          return this.text;
        }

        @action changeText(text) {
          this.text = text;
        }
      }
    );

    this.owner.register(
      'template:components/parent-component',
      hbs`
        <div data-dummy>{{this.message}}</div>
        {{yield (hash changeText=this.changeText)}}
      `
    );

    this.set('show', true);

    await render(
      hbs`
        <ParentComponent as |parent|>
          {{#if show}}
            <div
              {{did-insert (fn parent.changeText "Hello")}}
              {{will-destroy (fn parent.changeText "World")}}
            >
            </div>
          {{/if}}
          <button data-button {{on "click" (fn parent.changeText "World")}}>Change Text</button>
        </ParentComponent>
      `
    );

    // did-insert invokes re-rendering correctly, now the message is "Hello".
    assert.strictEqual(find('[data-dummy]').innerText, 'Hello'); 

    // trigger destroying. should change text.
    this.set('show', false);

    // will-destroy does not invoke re-rendering. message supposed to be "World".
    assert.strictEqual(find('[data-dummy]').innerText, 'Hello'); 

    await click('[data-button]');
    // if the changeText function is called by other ways, it works seamlessly.
    assert.strictEqual(find('[data-dummy]').innerText, 'World');
  });
});

Also attached some dependencies info here.

"devDependencies": {
  "@ember/optional-features": "^0.7.0",
  "@ember/render-modifiers": "^1.0.2",
  "@glimmer/component": "^0.14.0-alpha.13",
  "ember-cli": "~3.13.1",
  "ember-cli-babel": "^7.7.3",
  "ember-source": "~3.13.2"
},

Using with `mut` results in render error

Hello!

Say I have a component Test:
components/test/template.hbs
<div {{did-insert (fn (mut this.element))}} {{will-destroy (fn (mut this.element) null)}} />

When that component is destroyed, it results in the following error:
You modified "element" twice on [object Object] in a single render. It was rendered in undefined and modified in undefined. [...]

If I instead perform the registering/unregistering like so:
components/test/template.hbs
<div {{did-insert this.register}} {{will-destroy this.unregister}} />

components/test/component.js

@action
register(element) {
  this.element = element;
}

@action
unregister() {
  this.element = null;
}

Destroying the component doesn't result in an error.

This happens when mut-ing any property with any value on destroy, by the way, not just an element or a property that has been set before.

For reference, here's the code I used to test it:

{{#if this.show}}
  <Test />
{{/if}}

<Button @onClick={{toggle-action 'show' this}}>Toggle</Button>

Also, I'm not sure it makes any difference, but I'm on a canary build of Ember:
"ember-source": "https://s3.amazonaws.com/builds.emberjs.com/canary/shas/55f876ebc10bd1645e1e62fc5e0408266952259b.tgz"
"ember-cli": "github:ember-cli/ember-cli#ba9e3ea9bcad1c6e2299e40fe265cffe61d7a25b"

Any help would be appreciated!

UPDATE: It works if I use the action helper instead of fn. It also passes if I use ember-fn-helper-polyfill for fn. If I use the Ember 3.11 version of fn however, it fails with the error specified above.

Timing issues without didRender

In the process of rewriting components to Octane and using modifiers I ran into timing issues when didRender could no longer be used. The component in question renders text and based on if the text is scrollable or not adds an ‘expand text’ button, which when clicked shows the whole text without having to scroll.

See the following image for some explanation:
truncated-toggle

  1. Short text that is not scrollable
  2. Long text that scrolls, button is shown
  3. When the long text is expanded (There is also a button to collapse the content, but forgot to add it in the image)
  4. New text is loaded, should show button

The problem occurs when you expand the text and then load in new text. Every time new text gets loaded the text gets collapsed by default, then it checks if the text is scrollable. But because it takes some time to render the collapsing it does not see the text as scrollable and no button is shown.

Component.js :

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'

export default class TruncatedToggle extends Component {
  @tracked
  hasOverflow = false;

  @tracked
  opened = false;

  @action
  init() {
    this.opened = false;
  }

  @action
  getOverflow(element) {
    this.hasOverflow =  element.scrollHeight-1 > element.clientHeight;
  }

  @action
  buttonClicked() {
    this.opened = !this.opened;
  }
}

Previously the getOverflow code was in didRender in which the element scrollHeight and clientHeight gave correct values. Tried to set opened in the init() function to have some time between getting the overflow and collapsing the content, but that was in vain.

Component hbs:

<div class="truncated-toggle"
  {{did-update this.init @content}}
>
  <div class="truncated-toggle__content"
    {{did-insert this.getOverflow}}
    {{did-update this.getOverflow @content}}
  >
    {{{@content}}}
  </div>

  {{#if this.hasOverflow}}
    <button {{action 'buttonClicked'}} class="btn btn--default">
      {{#if this.opened}}
        Collapse text
      {{else}}
        Expand text
      {{/if}}
    </button>
  {{/if}}
</div>

I am able to make this work by adding a timeout:

  @action
  getOverflow(element) {
    setTimeout(() => {
      this.hasOverflow =  element.scrollHeight-1 > element.clientHeight;
    })
  }

But this feels a bit hacky to me. Is this the way to go, or is there a solution that I do not know about?

Weird issue when referencing `document` in callback of will-destroy

I'm not sure how/if this is really related to the modifiers, but I haven't been able to reproduce it without them. I believe it's some kind of interaction between babel/decorators/modifiers.

Apparently if in the callback passed to {{will-destroy}} I use the global document without having referenced it elsewhere, the compilation blows badly:

ReferenceError: document is not defined
    at BasicDropdownTrigger.removeGlobalHandlers (/var/folders/bt/62lj_mr106zbk82m8kl772b00000gn/T/broccoli-9932Xc3l4ZKiotlD/out-271-broccoli_merge_trees/assets/addon-tree-output/ember-basic-dropdown/components/basic-dropdown-trigger.js:140:1)
    at Object.destroyModifier (/var/folders/bt/62lj_mr106zbk82m8kl772b00000gn/T/broccoli-9932Xc3l4ZKiotlD/out-271-broccoli_merge_trees/assets/addon-tree-output/@ember/render-modifiers/modifiers/will-destroy.js:26:1)
    at CustomModifierState.destroy (/var/folders/bt/62lj_mr106zbk82m8kl772b00000gn/T/broccoli-9932Xc3l4ZKiotlD/out-271-broccoli_merge_trees/assets/vendor/ember-modifier-manager-polyfill.js:58:1)
    at SimpleBlockTracker.destroy (/var/folders/bt/62lj_mr106zbk82m8kl772b00000gn/T/broccoli-9932Xc3l4ZKiotlD/out-271-broccoli_merge_trees/assets/@glimmer/runtime.js:2411:1)
    at UpdatableBlockTracker.destroy (/var/folders/bt/62lj_mr106zbk82m8kl772b00000gn/T/broccoli-9932Xc3l4ZKiotlD/out-271-broccoli_merge_trees/assets/@glimmer/runtime.js:2411:1)

The reproduction can be this PR: cibernox/ember-basic-dropdown#449

If (this line)[https://github.com/cibernox/ember-basic-dropdown/pull/449/files#diff-e8b1232b23cd9aba9a4b4c3f74b118fcR18] referencing the global document is removed the code fails to compile.

Dependency error when using Glint types (`@glint/template`)

When following the Glint setup as described in the README, I'm seeing the following error.

../node_modules/@ember/render-modifiers/types/-private.d.ts:1:30 - error TS2307: Cannot find module '@glint/template' or its corresponding type declarations.

import { ModifierLike } from '@glint/template';

Monorepo environment:

  • Node.js v18.18.2
  • pnpm 8.6.5
  • ember-source 4.12.3

In the addon, locally declared as devDependency:

"@glint/core": "^1.2.1",
"@glint/environment-ember-loose": "^1.2.1",
"@glint/template": "^1.2.1",

The dependency is declared as peer in package.json, which seems correct so I'm kind of puzzled why this error is popping up.

For now we're using glint-template-types.

modifierManagerCapabilities is not a function

We've seen this error popping up at various projects. The reason being we have the dependency: "@ember/render-modifier": "^1.0.0". With 1.0.0 it was working fine but after an upgrade to 1.0.1 we are seeing this error message:

Uncaught (in promise) TypeError: Ember._modifierManagerCapabilities is not a function
    at vendor.js:89048
    at RuntimeResolver._lookupModifier (vendor.js:20216)
    at RuntimeResolver.lookupModifier (vendor.js:20127)
    at CompileTimeLookup.lookupModifier (vendor.js:16613)
    at LazyCompiler.resolveModifier (vendor.js:52478)
    at vendor.js:51567
    at Compilers.compile (vendor.js:51537)
    at compile (vendor.js:52230)
    at LazyCompiler.add (vendor.js:52424)
    at CompilableProgram.compile (vendor.js:52188)

Our workaround was to specify the version of this package to v1.0.0. That said it doesn't necessarily have to be this package cause it, but can be a very mixed constellation of packages that is causing it.

@model hash and did-update don't play well together

I've created a reproduction of this issue which I'll attempt to describe here:

When returning a hash from the routes model hook and then passing that hash into a component as split properties <Consumer @one={{@model.one}} @two={{@model.two}} /> and then using those split properties in getters of the component to invoke did-update an exception will be thrown when leaving the route. You can see this in the reproduction by navigating between the routes with the console open. This same thing doesn't happen if this.model is used in place of @model.

What I suspect is happening is that @model gets torn down early and re-assigned to the incoming route. This causes the first property @model.first to change which triggers did-update to call the second getter which is now unable to access this.args.second because it is also gone.

I've reproduced this as a synchronous action, but we're hitting this issue when did-update calls a task which loads async state. That's why the modifier needs to trigger the action and it can't just be a getter itself.

I'm not sure if this is an issue with did-update or a broader issue with modifiers and tracked properties using the @model pattern, but this seemed like the right place to start.

Error: Invalid modifier manager compatibility specified

Hi, I'm having some issues with the helpers when using the latest version (2.0.4) with Ember version 4.4.0. Using did-insert in a template triggers the following: Error: Invalid modifier manager compatibility specified.

New release with ember v5 support?

With #70 being merged, and now that ember v5 is in beta channel, I think a new release is warranted here to so that consumers of this addon can test against beta without receiving a version warning.

Documentation!

  • API docs
    • did-insert
    • did-update
    • will-destroy
  • General usage guides
    • Copy usage examples from emberjs/rfcs#415 (#3)
    • Identify common example scenarios, document how to handle them...

Modifiers don't seem to work when applied to elements are ember components with `tagName=''`

Using modifiers on an element that is itself a classic Ember component (@ember/component) doesn't seem to work.
This poses difficulties because the developer now has to 'know' :godmode: what the
other component is.

This works great:

<div {{did-insert this.someFunc}}>
</div>

This not so much (doesn't error, doesn't execute).

<SomeOtherEmberComponent {{did-insert this.someFunc}}>
</SomeOtherEmberComponent>

Unexpected token '.' in Gitlab pipeline

After installing ember-render-modifiers version 2.0.3 or 2.0.4 the local building process works fine, but the Gitlab pipeline fails with the error Unexpected token '.' and nothing else in the logs. We use a docker container in the pipeline that executes ember build --environment=test. Then it builds, cleans up and logs the error message.

Is there anything I can do to get more info about that error? Has anyone encountered that before?

Support conditional modifiers

Since (to my knowledge) its not possible to conditionally add a modifier. Something like this:

// doesnt work
<div
  {{if @onInsertTextArea (did-insert @onInsertTextArea)}}
/>
// doesnt work
<div
  {{#if @onInsertTextArea}}
	did-insert @onInsertTextArea
  {{/if}}
/>

then maybe the modifiers provided by this library could silently handle undefined functions, so you could do:

<div
  {{did-insert @onInsertTextArea}}
/>

and when @onInsert is undefined it wont throw an error, like it does now.

Either make the current modifiers silently handle undefined or make new ones to handle modifier function that might no always be provided. This is mostly for general components.

Another option might be to provide -if version of the modifiers:

<div
  {{did-insert-if @onInsertTextArea @onInsertTextArea}}
/>

Improve the error message when `this` is undefined

If you try to use this within methods called by {{on}} and {{fn}}, they show a helpful error instructing the developer to use the @action decorator. The render modifiers could show something like that too.

Here's an example "error" use case for did-insert:

<div class="my-container" {{did-insert this.renderEditor}}></div>
import Component from '@glimmer/component';

export default class CodeEditor extends Component {
  renderEditor(el) {
    console.log(this.args.code)
  }
}

TypeError: this is undefined

{{fn}} uses this error message:

You accessed this.arg2 from a function passed to the fn helper, but the function itself was not bound to a valid this context. Consider updating to usage of @action.

Octane needs types added in resolver

at present, these need to be added manually:

  // required for @ember/element-modifiers
  modifier: { definitiveCollection: 'main' },

in src/resolver.js.

Whenever there are hooks for addons to do this themselves, this addon should add the modifier collection.

Node 12/14 unsupported?

As Node 10 is coming up on EOL, I tried to update my project to Node 14 (and 12). In both cases, when I did, I got the following build error:

Build Error (broccoli-persistent-filter:Babel > [Babel: @ember/render-modifiers]) in @ember/render-modifiers/modifiers/did-insert.js

No "exports" main defined in /Users/stephen.weiss/code/olo/expo-web-client/node_modules/@ember/render-modifiers/node_modules/@babel/helper-compilation-targets/package.json


Stack Trace and Error Report: /var/folders/dz/43k2z2yn53g6g5wgzbx0gw8m0000gp/T/error.dump.f35dc8ac0b4685c4fa73ca1fd5ee7483.log

Is there anything we can do to accommodate newer Node versions? Or is there any recommended path forward?

My believe is that I am currently only using this in one place:

<form onKeyPress={{action this.handleKeyPress}} onSubmit={{action this.handleFormSubmit}}
          {{did-insert this.handleFormInsert}} tabindex="0">
    //...
</form>

where handleFormInsert is really meant to just add focus:

  handleFormInsert(element) {
    element.focus();
  }

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.