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

Initial implementation of model inputs #54252

Closed
wants to merge 16 commits into from

Conversation

crisbeto
Copy link
Member

@crisbeto crisbeto commented Feb 5, 2024

Adds support for model inputs in the framework. model() returns a writable signal that implicitly defines a input/output pair that can be used either in two-way bindings to keep two values in sync or by binding individually to the input and output. When the value of the model changes, it will emit an event with the current value.

Furthermore, these changes expand two-way bindings to accept WritableSignal. This will make it easier to transition existing code to signals in a backwards-compatible way.

Example:

@Directive({
  selector: 'counter',
  standalone: true,
  host: {
    '(click)': 'increment()',
  }
})
export class Counter {
  value = model(0);

  increment(): void {
    this.value.update(current => current + 1);
  }
}

@Component({
  template: `<counter [(value)]="count"/> The current count is: {{count()}}`,
})
class App {
  count = signal(0);
}

Note: with these changes model inputs should be mostly working. There are a few places where things will be improved in a follow-up PR:

  • Ensuring that the language service works correctly with the new TCB for two-way bindings.
  • Avoid having to return an object literal from ModelSignal.subscribe.
  • Figure out the performance implications of checking if the value is a signal each time the twoWayProperty instruction is called.
  • Figure out if we should remove applyValueToInputSignal from MODEL_SIGNAL_NODE.
  • Avoid the extra variable in the two-way binding TCB.
  • Adding a diagnostic when input() is mixed with @Output() in a two-way binding.

@crisbeto crisbeto added action: review The PR is still awaiting reviews from at least one requested reviewer area: core Issues related to the framework runtime target: minor This PR is targeted for the next minor release labels Feb 5, 2024
@ngbot ngbot bot added this to the Backlog milestone Feb 5, 2024
@angular-robot angular-robot bot added the detected: feature PR contains a feature commit label Feb 5, 2024
@kbrilla
Copy link

kbrilla commented Feb 5, 2024

Questions:

  1. In example we have model(0) and singal(0) so both have same starting value, but which one will win in scenario model(5) and signal(0) ?
  2. Would it be possible to make model required? Or at least input side of model required so there is no need for default value?

@crisbeto
Copy link
Member Author

crisbeto commented Feb 5, 2024

model(0) sets the default value and it'll be overwritten by the one coming from the property binding.

@crisbeto crisbeto force-pushed the model-inputs branch 3 times, most recently from bfbb7c1 to e17685b Compare February 5, 2024 18:59
@crisbeto crisbeto marked this pull request as ready for review February 5, 2024 19:24
packages/core/src/core.ts Outdated Show resolved Hide resolved
Copy link
Member

@devversion devversion left a comment

Choose a reason for hiding this comment

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

LGTM overall, mostly nits/questions. Nice! 🎉

packages/core/src/authoring/model/model_signal_node.ts Outdated Show resolved Hide resolved
packages/compiler-cli/src/ngtsc/metadata/src/api.ts Outdated Show resolved Hide resolved
packages/compiler-cli/src/ngtsc/testing/fake_core/index.ts Outdated Show resolved Hide resolved
@@ -199,6 +199,33 @@ runInEachFileSystem(() => {
.toContain(`inputs: { data: [i0.ɵɵInputFlags.SignalBased, "data"] }`);
});

// TODO(crisbeto): we may not want to support this combination. Will discuss with the team.
Copy link
Member

Choose a reason for hiding this comment

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

Good conversation. I believe we want to support use-cases where the local "state" might be desychronized from the parent. There is a good issue about people asking for this- although in practice we couldn't come up with a good scenario where users may not want to properly synchronize parent & component (e.g. Dir#expanded)

Copy link
Member

Choose a reason for hiding this comment

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

This combination is fine - there's an input and output of the right shape for two-way binding. We can't enforce that components conform to the two-way binding contract.

packages/compiler-cli/test/ngtsc/authoring_spec.ts Outdated Show resolved Hide resolved
ts.isAsExpression(node.left.expression.expression) &&
isAccessExpression(node.left.expression.expression.expression)) {
assignment = node.left.expression.expression.expression;
}
Copy link
Member

Choose a reason for hiding this comment

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

Maybe already planned as a follow-up: Can you please add a test for the language service. Definition + completions? I invested a lot in writing tests here and I think we should continue doing this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah that's one of my points for the follow-up. I just wanted to get all the foundations in place in this PR.

crisbeto added a commit to crisbeto/angular that referenced this pull request Feb 6, 2024
@crisbeto
Copy link
Member Author

crisbeto commented Feb 6, 2024

I've reworked the TCB to avoid some regressions. Also addressed all the feedback in the last commit.

standalone: true,
})
export class TestDir<T> {
value = model.required<T>();
Copy link
Member

Choose a reason for hiding this comment

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

We should also have a test to ensure the value property is inferred properly. This is a follow-up I guess?

crisbeto added a commit to crisbeto/angular that referenced this pull request Feb 6, 2024
@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

@JeanMeche model() is ready to be used. There are some internal cleanups to do, but they won't affect the behavior.

@wartab
Copy link
Contributor

wartab commented Feb 9, 2024

I know things aren't completed yet, but is there anything planned at framework level for forms? Or is the following method the way to go for the foreseeable future?

<input [ngModel]="signal()" (ngModelChange)="signal.set($event)">

@sysmat
Copy link

sysmat commented Feb 9, 2024

@wartab yes as classical prop as signal, for now I'm doing it this way

@sysmat
Copy link

sysmat commented Feb 9, 2024

@JeanMeche so is there another directive for model?

@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

@sysmat I just tried your example with the RC and it seems to work. See https://stackblitz.com/edit/stackblitz-starters-dcjjnw?file=src%2Fmain.ts

@wartab two-way binding to a signal is supported with this change. You should be able to do [(ngModel)]="someSignal".

@wartab
Copy link
Contributor

wartab commented Feb 9, 2024

Really cool, thank you very much

@sysmat
Copy link

sysmat commented Feb 9, 2024

@crisbeto yes it works with signal, but not with model is a bit of mystery to me why not with model, probably I'm missing something here

@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

It works for me with model() as well. Here's a fork of the previous commit: https://stackblitz.com/edit/stackblitz-starters-kij9th?file=src%2Fmain.ts. Maybe you haven't updated the compiler to the latest RC?

@sysmat
Copy link

sysmat commented Feb 9, 2024

  • I have
"@angular/compiler-cli": "^17.2.0-rc.0",
"@angular/compiler": "^17.2.0-rc.0",
  • @crisbeto super, yes I don't now why in my local setup dosen't work

@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

It may be worth clearing your node_modules and reinstalling.

@sysmat
Copy link

sysmat commented Feb 9, 2024

@crisbeto I delete lock, re install, and also using "builder": "@angular-devkit/build-angular:application",

@sysmat
Copy link

sysmat commented Feb 9, 2024

@crisbeto in your stackblitz you are using, is this ok

"devDependencies": {
    "@angular-devkit/build-angular": "~0.1100.4",
    "@angular/cli": "~11.0.4",
    "@angular/compiler-cli": "~11.0.4",
    "@types/jasmine": "~3.6.0",
    "@types/node": "^12.11.1",
    "codelyzer": "^6.0.0",
    "jasmine-core": "~3.6.0",
    "jasmine-spec-reporter": "~5.0.0",
    "karma": "~5.1.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.0.3",
    "karma-jasmine": "~4.0.0",
    "karma-jasmine-html-reporter": "^1.5.0",
    "protractor": "~7.0.0",
    "ts-node": "~8.3.0",
    "tslint": "~6.1.0",
    "typescript": "~4.0.2"

@sysmat
Copy link

sysmat commented Feb 9, 2024

my component is loaded by router, but this shouldn't be problem, when it works with signal

{
    path: 'model',
    loadComponent: async () => (await import('./app/ng-model.component')).SelectModelSignalComponent,
  }

@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

Hmm that's weird. Here's a fork with 17.2 of the CLI: https://stackblitz.com/edit/stackblitz-starters-9pipk1?file=package.json

@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

Following up here: we've released version 17.2.0-rc.1 to resolve the type checking error that @JeanMeche flagged above.

@sysmat
Copy link

sysmat commented Feb 9, 2024

  • when creating fresh project with cli, it works
Angular CLI: 17.2.0-rc.0
Node: 20.11.0
Package Manager: npm 10.2.4
OS: win32 x64

Angular:
...

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1702.0-rc.0 (cli-only)
@angular-devkit/core         17.2.0-rc.0 (cli-only)
@angular-devkit/schematics   17.2.0-rc.0 (cli-only)
@schematics/angular          17.2.0-rc.0 (cli-only)
"dependencies": {
    "@angular/animations": "^17.2.0-next.0",
    "@angular/common": "^17.2.0-next.0",
    "@angular/compiler": "^17.2.0-next.0",
    "@angular/core": "^17.2.0-next.0",
    "@angular/forms": "^17.2.0-next.0",
    "@angular/platform-browser": "^17.2.0-next.0",
    "@angular/platform-browser-dynamic": "^17.2.0-next.0",
    "@angular/router": "^17.2.0-next.0",
  • VERSION.full --> 17.2.0-rc.1

  • model & select babnana in the box work

<select [(ngModel)]="selectedModel">
        <option [value]="7">7</option>
        <option [value]="14">14</option>
</select>
<hr />
Selected value: {{ selectedModel() }}

....
version = VERSION.full;
selectedModel = model(7);
  • so with my old project started as ng 9, doing ng update and migrations, is not working with model but it works with signal

@sysmat
Copy link

sysmat commented Feb 9, 2024

  • in my new project when using routing then not working model & [()]
export const routes: Routes = [
    {
        path: 'model',
        loadComponent: async () => (await import('./ng-model.component')).SelectModelSignalComponent,
      },
];
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, 
    withComponentInputBinding(),
    withViewTransitions(),
    withInMemoryScrolling({
      scrollPositionRestoration: 'enabled',
    }))]
};

@sysmat
Copy link

sysmat commented Feb 9, 2024

I take fork of @crisbeto with routing and it is not working on route (model.ts), but work in main

@crisbeto
Copy link
Member Author

crisbeto commented Feb 9, 2024

I'm guessing this is an issue when setting the value through setInput, which is used by the router.

@sysmat
Copy link

sysmat commented Feb 9, 2024

yes, the withComponentInputBinding() is the problem

@scottwalter-nice
Copy link

yes, the withComponentInputBinding() is the problem

If I have a component that has no inputs but have withComponentInputBinding() enabled. Declaring a model such as
modelName = model('Model Scott'); will not appear in the template: {{ modelName()}}

Unless I set the value in ngInit: modelName.set('Model Scott');

@sysmat
Copy link

sysmat commented Feb 9, 2024

@scottwalter-nice but why should be model bound to router, for that we have input

@scottfwalter
Copy link

@scottwalter-nice but why should me model bound to router, for that we have input

I was probably misusing the shiny new model when input or a simple Signal would have sufficed. I guess we need a good explainer on basic SIgnal vs model.

@crisbeto
Copy link
Member Author

@sysmat looking into your Stackblitz: I think the problem is that you aren't setting a value for the selectedModel parameter so the router sets it to undefined. E.g. if I set the link to [routerLink]="['/model', {selectedModel: 7}]" it works fine. Here's an updated Stackblitz https://stackblitz.com/edit/stackblitz-starters-gjjmie.

@scottwalter-nice
Copy link

@sysmat looking into your Stackblitz: I think the problem is that you aren't setting a value for the selectedModel parameter so the router sets it to undefined. E.g. if I set the link to [routerLink]="['/model', {selectedModel: 7}]" it works fine. Here's an updated Stackblitz https://stackblitz.com/edit/stackblitz-starters-gjjmie.

To be honest I’m confused between signal inputs and model. I originally thought model was for two way binding, but i can do that with regular signals on 17.2

What is the use case of model vs signal inputs?

@crisbeto
Copy link
Member Author

  1. model() is a shorthand to define a two-way binding.
  2. model signals are writable from inside the component while the input() signals are a read-only.

@imaksp
Copy link

imaksp commented Mar 5, 2024

@scottwalter-nice but why should me model bound to router, for that we have input

I was probably misusing the shiny new model when input or a simple Signal would have sufficed. I guess we need a good explainer on basic SIgnal vs model.

yes I also used new model for ngModel field initially but it didn't work with 'withComponentInputBinding', should have tried this normal signal first. I assume its not meant for using with [(ngModel)].

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Apr 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime detected: feature PR contains a feature commit target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet