Skip to content
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

feat(angular-table): adds Angular adapter for tanstack/table (#5326) #5432

Merged
merged 45 commits into from May 12, 2024

Conversation

KevinVandy
Copy link
Member

  • Angular adapter for tanstack table initial

  • build with: ng packagr

  • Add angular examples basic grouping

  • Add angular examples basic grouping

  • Update angular examples basic grouping

  • Add angular example selection

  • regen lock file

  • package upgrades, angular table docs

  • prettier

  • move around some deps

  • removed unused dependency from angular package

  • fix deps


* Angular adapter for tanstack table initial

* build with: ng packagr

* Add angular examples basic grouping

* Add angular examples basic grouping

* Update angular examples basic grouping

* Add angular example selection

* regen lock file

* package upgrades, angular table docs

* prettier

* move around some deps

* removed unused dependency from angular package

* fix deps

---------

Co-authored-by: Kevin Van Cott <[email protected]>
Copy link

nx-cloud bot commented Mar 25, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 529bc29. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 1 target

Sent with 💌 from NxCloud.

@KevinVandy
Copy link
Member Author

@imaksp @danielglejzner @crutchcorn @jrgokavalsa @Lord-AY @JeanMeche @tonivj5 Work continues on this branch for the TanStack Angular Table adapter. Anyone can make a PR to this branch instead of just code reviewing the original PR.

Things that need to be done:

  1. Convert the angular adapter to go all in on Angular 17 signals. After discussion with @crutchcorn, we've decided that we should only support modern angular going forward, in order to simplify support, and provide the best performance for complicated data-grid features. The min supported version will probably be v17.3 for TypeScript purposes.
  2. Rewrite the examples. The current examples in this PR seem bloated. Let's get the dependencies and configs to the bare minimum, and maybe consider using Vite like all the other examples (though this doesn't matter that much). Also, encourage the use of signals for state.
  3. Write the Table State (Angular) docs with accurate information and syntax.
  4. Get the dependencies, devDependencies, and peerDependencies in check for the adapter and the examples.
  5. Bonus: Create sorting, filtering, and pagination examples since those are somewhat essential for those seeing TanStack Table for the first time.

After these 4-5 things are accomplished, we'll feel good about shipping this. I would appreciate help with these tasks, as Angular is the framework I am least familiar with.

@riccardoperra
Copy link
Contributor

riccardoperra commented Mar 25, 2024

Hi, I'm currently working on a design system that will use angular w/ tanstack table. These days I already had to work on my own on an adapter that use signals, I'm following you to understand if I can directly integrate the official one, or keep ours anyway.

I still have some doubts about my current implementation, I tried these approaches:

  • return a table signal that updates on every option/state change (wrapped with lazy-signal-initializer in order to support required input signals)
const table = createAngularTable(() => ({
   ...
});
// re-run automatically on every change 
table().options.state
table().getHeaderGroups()
table().getRowModel() 
...
  • an approach like @tanstack/angular-query (signal-proxy.ts), where I return a proxified table object, which wraps in a computed:
    • every plain property (like options, rowCount, etc...)
    • every function that starts with get (like getState(), getPageCount() etc...)
    • the computed will trigger on every state/option change
const table = createAngularTable(() => ({
   ...
});
table.options().state // `options` transformed from plain property to computed
table.getHeaderGroups() // This is a computed
table.getRowModel() // This is a computed
table.set... // Still same property
...

One problematic thing I'm trying to solve is supporting onPush change detection everywhere, so with the first approach I think it should be ok, but only if you don't memorize the signal or if you create a new reference of the object. The second one, however, could be riskier as I'm not sure that all the properties must be transformed into computed in the same way, or there is a risk that the table will not update correctly due to extra memoization.

This is what I'm using at the moment, hope it can help and if you can give me some advice 😄

export function createAngularTable<TData extends RowData>(
  options: () => TableOptions<TData>,
) {
  const injector = inject(Injector);
  // Supports required signal input (https://angular.io/errors/NG0950)
  return lazySignalInitializer(() => {
    return runInInjectionContext(injector, () => {
      const resolvedOptionsSignal = computed<TableOptionsResolved<TData>>(
        () => {
          return {
            state: {},
            onStateChange: () => {},
            renderFallbackValue: null,
            ...options(),
          };
        },
      );

      const table = createTable(resolvedOptionsSignal());
      const tableSignal = signal(table);
      const state = signal(table.initialState);

      function updateOptions() {
        const tableState = state();
        const resolvedOptions = resolvedOptionsSignal();
        table.setOptions(prev => ({
          ...prev,
          ...resolvedOptions,
          state: {...tableState, ...resolvedOptions.state},
          onStateChange: updater => {
            if (updater instanceof Function) {
              state.update(updater);
            } else {
              state.set(updater);
            }
          },
        }));

        // Spreading this otherwise signal with default `equals` will not trigger on state change
        // Another solution could be using `equal: () => false` on `lazySignalInitializer` and `tableSignal`
        untracked(() => tableSignal.set({...table}));
      }

      updateOptions();

      // Currently I'm using this to avoid updating options twice the first time.
      let skipUpdate = true;
      effect(() => {
        void [state(), resolvedOptionsSignal()];
        if (skipUpdate) {
          skipUpdate = false;
          return;
        }
        untracked(() => updateOptions());
      });

      return table;
    });
  });
}

One another important thing about flexRender

  • is it correct that with the current implementation once rendered, the component will not be updated anymore?
    ) {}
    ngOnInit(): void {
    // This ensures that if the 'flexRender' input is set before the directive initializes,
    // the component will be rendered when ngOnInit is called.
    if (this._flexRender) {
    this.renderComponent()
    }
    }

    Everything is placed inside the onInit, so if the "context" changes, the component would no longer be updated

@KevinVandy
Copy link
Member Author

@riccardoperra, your described situation makes you sound like a god-send. lol

I'm not against totally re-implementing the adapter part of this PR if needed. Not that the original PR was bad, but signals are just going to be much better if we're requiring Angular 17+ anyway. I'd love @crutchcorn to review your adapter code, because my angular knowledge is limited. The initial state stuff looks good though.

You can make a PR to the branch that this PR is made from. If the adapter is changed, get at least 1 example using state (row selection) updated too if you could. We don't need 100% comprehensive PRs yet.

@crutchcorn
Copy link
Member

I'll willingly admit that I haven't spent as much time in Angular signal land as you have @riccardoperra but your code LGTM I'd merge it (assuming tests passed)

Let's start a PR to keep track of your changes and we can do more minutia code review and testing.

And, of course, thank you so much for your help!!

}
}

renderComponent() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this method supposed to be public ?


renderComponent() {
this.vcr.clear()
if (!this._flexRender) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early return seem redundant with the following conditions

@riccardoperra
Copy link
Contributor

Hi, I've added something here 😄 #5442

@riccardoperra
Copy link
Contributor

Hi @KevinVandy, I'm answering you for this, sorry for the late reply.

Unfortunately I had to pause the work on our table in our design system due to some priority needs. I accidentally pushed a few things that broke the pr examples and the build, which I can fix soon. Anyway, The current integration here is the latest one we have in our ds which seems working fine.

I would like to get some feedbacks from someone who works with angular to understand if what I did with the proxy approach is ok. Regardless, the returned object is a signal so it should work fine. We personally had to memorize some functions to optimize performance with this approach, but I think it's "normal"

@KevinVandy
Copy link
Member Author

Thinking of closing this PR if we cannot make more progress on it. We need someone who is knowledgeable in Angular to not just advise, but actually make the adapter and examples.

@merto20
Copy link
Contributor

merto20 commented May 10, 2024

@merto20 No need to ask for permission to make improvements. Just make the PR

@riccardoperra I rewrote the adapter and table state docs for angular based off your examples. Only thing I left a TODO in was for how to create fully controlled state. Guessing it would require computed just like the adapter does it.

@KevinVandy will do.

fully controlled state should also work using input, signal, and model or any signal based types.

@KevinVandy
Copy link
Member Author

As far as I'm concerned, there are just 2 more things to take care of this weekend before shipping.

  1. I'm having @arnoud-dv advise on some build config stuff, and he also offered to review the PR more
  2. Document Angular Signals and Table State a bit more in the table-state docs.

@arnoud-dv
Copy link
Contributor

arnoud-dv commented May 12, 2024

  1. Rewrite the examples. The current examples in this PR seem bloated. Let's get the dependencies and configs to the bare minimum, and maybe consider using Vite like all the other examples (though this doesn't matter that much). Also, encourage the use of signals for state.

The examples for Query were in Vite before but I converted them to Angular CLI for two reasons:

  • Vast majority of Angular devs will use the CLI so examples are more representative
  • Not all Angular functionality was working correctly such as input signals

I would recommend staying on CLI, also the Angular CLI uses Vite internally

@riccardoperra
Copy link
Contributor

riccardoperra commented May 12, 2024

I've added some pr for documentation here:

We may have to discuss about this case before releasing the library

@arnoud-dv
Copy link
Contributor

arnoud-dv commented May 12, 2024

@riccardoperra are the examples working correctly while developing the adapter? We've had trouble in Query before as there were two package.json files, one in the src directory and one in the build directory. It resulted in somewhat random errors where you'd had to run pnpm i and pnpm build until it got in some working (but still unreliable) state. Also when publishing the Angular query package for the first time we got an error. We fixed that in this commit: TanStack/query@4e8a6c5

@riccardoperra
Copy link
Contributor

riccardoperra commented May 12, 2024

@arnoud-dv yes, in my environment examples seems working fine while developing, but i always call the build command manually after I made a change. If using watch, we should have thedeleteDestPath set to false (already updated). I remember about the error you mentioned but I didn't had to do in this repo 🤔 Could be something related to the nx cache

Also when publishing the Angular query package for the first time we got an error. We fixed that in this commit:

I forgot about the replicated package.json. Currently I think it will be broken, we have to fix it.

In my company we always published via changesets the /dist folder (through a configuration in the package.json) but we have to remove through script the .npmignore generated file and sync the package.json version. We had a lot of sub-entry so doing it manually in the root package.json was a waste of time.

Looking at the commit, you simply add the exports manually and publish the "root" folder, right? Are you also publishing the source code?

@arnoud-dv
Copy link
Contributor

arnoud-dv commented May 12, 2024

Looking at the commit, you simply add the exports manually and publish the "root" folder, right? Are you also publishing the source code?

Yes, but I did recently remove the source code from the published package. The published files are determined using files in package.json.

"files": [
  "build",
  "!**/*.d.ts",
  "!**/*.d.ts.map",
  "build/rollup.d.ts"
],

Note I also now exclude all type declarations except a rolled up one generated by Microsoft API extractor. Flattening type declarations using API extractor is recommend by the Angular package format for good reasons IMO but I see few Angular packages actually do this. If you want I could add a PR next week to do that for Table too? Could be done after a first package publish too. Also I like the API report API extractor generates, it allows to keep oversight of the public API easily.

@arnoud-dv
Copy link
Contributor

I remember about the error you mentioned but I didn't had to do in this repo 🤔 Could be something related to the nx cache

Might have been an issue in the pnpm version used back then, pnpm got confused and would also change the package location in lockfile between the two package.json locations constantly.

@arnoud-dv
Copy link
Contributor

Looks good to me, will there be a beta period after publishing where breaking changes could be made if necessary?

Very cool to have TanStack Table on Angular too ❤️

@KevinVandy
Copy link
Member Author

Looks good to me, will there be a beta period after publishing where breaking changes could be made if necessary?

Very cool to have TanStack Table on Angular too ❤️

If breaking changes are needed to fix something in the first few weeks before a ton of people are using this, that's fine. Though we probably won't be giving the packages beta versions, unless we figure out configuration for that.

riccardoperra and others added 3 commits May 12, 2024 09:40
* tslib should be a dependency, see:

https://angular.io/guide/angular-package-format#tslib

* ensure angular examples are run as web container on code sandbox

* update lock file

---------

Co-authored-by: Kevin Vandy <[email protected]>
* add flexRender documentation

* fix docs

* fix rendering component section heading
@KevinVandy
Copy link
Member Author

KevinVandy commented May 12, 2024

@arnoud-dv

Also when publishing the Angular query package for the first time we got an error. We fixed that in this commit: TanStack/query@4e8a6c5

Looks like we actually still have some inconsistencies in our package package.json, especially with "files". Thanks for linking that query PR.

Edit: maybe our implementation is ok. We might not find out until shipping...

We're also esm only right now. Wonder if this needs more consideration.

image

@arnoud-dv
Copy link
Contributor

arnoud-dv commented May 12, 2024

Looks like we actually still have some inconsistencies in our package package.json, especially with "files". Thanks for linking that query PR.

It's not the same as Query but it's correct I'd say. If we want to implement type declaration rollup using API extractor we could change it but it's optional.

Edit: there is the potential issue with the duplicate package.json but it seems to work fine so far in Table. If not, the same fix could be applied as in Query.

We're also esm only right now. Wonder if this needs more consideration.

That's correct, Angular packages are ESM-only.

@KevinVandy
Copy link
Member Author

KevinVandy commented May 12, 2024

I don't think there is much more to review until we try shipping it, so will try to do that shortly. Thanks for all the help so far @arnoud-dv @riccardoperra @merto20 @jrgokavalsa
Chances are that there are still some issues that will need to be ironed out, so any help that you are able to give this week will be very appreciated.

return this.renderComponent(content)
}

private renderStringContent() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be great to explicit the return types.

@KevinVandy KevinVandy merged commit 91f4360 into main May 12, 2024
2 checks passed
@KevinVandy KevinVandy deleted the feat-angular-table branch May 12, 2024 19:19
@KevinVandy
Copy link
Member Author

@merto20 Rendering is acting weird in the grouping example. That's the one you were testing your stuff with, right? fyi @riccardoperra

https://tanstack.com/table/latest/docs/framework/angular/examples/grouping

image

@merto20
Copy link
Contributor

merto20 commented May 18, 2024

@KevinVandy just got back after my vacation. This was fixed by @riccardoperra already. I will check what I can do on the 'flexRenderer'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants