3

I'm using AureliaJS to build a dynamic forms scenario, where I have a parent form with the gross operations needed and multiple child's form's, that change based on user input.

These child's form's have only two specific things themselves. Their model and the validation rules for their model.

So my question is, how can the parent form call the validation rules from the current child form? From child I know that is possible call parent's view model. But from parent, how can I invoke any function from the child?

The scenario is similar off having one base class, that has one method and this method could be overriding on the child classes.

Any suggestion? I'm glad to change the approach if needed.

Here's an example: https://gist.run?id=1865041a15af60600cb7b538018bdccd

app.html

<template>
  <span>This is an APP</span>
  </p>
  <compose view-model.bind="'parentForm'"></compose>
</template>

app.js

import { autoinject } from 'aurelia-framework';

@autoinject
export class App {

}

childForm1.html

<template>
  <label> Price : </label>
  <input value.bind="model.data.price">
  <p/>
  <label> VAT : </label>
  <input value.bind="model.data.vat">
  <p/>
</template>

childForm1.js

import { autoinject } from 'aurelia-framework';

@autoinject
export class ChildForm1 {

  activate(model)
  {
    this.model = model;
  }

 validateRules (){

     if(this.model.data.price != '' && this.model.data.vat == '' )
      this.model.validateMessage = 'VAT is mandatory';
 }
}

childForm2.html

<template>
  <label>Address : </label>
  <input value.bind="model.data.address">
  <p/>
  <label>Phone : </label>
  <input value.bind="model.data.phone">
  <p/>
</template>

childForm2.js

import { autoinject } from 'aurelia-framework';

@autoinject
export class ChildForm2 {

  activate(model)
  {
    this.model = model;
  }

 validateRules (){

   if(this.model.data.phone != '' && this.model.data.address == '' )
      this.model.validateMessage = 'Address is mandatory';
 }
}

index.html

<!doctype html>
<html>
  <head>
    <title>Aurelia</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body aurelia-app>
    <h1>Loading...</h1>

    <script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
    <script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
    <script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
    <script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
    <script>
      require(['aurelia-bootstrapper']);
    </script>
  </body>
</html>

parentForm.html

<template>
  <button click.delegate="changeChildForm1()">Change Child Form 1</button>
  <button click.delegate="changeChildForm2()">Change Child Form 2</button>
  <p/>
  <p/>
  <form>
    <label>User : </label>
    <input value.bind="model.data.user">
    <p/>
    <compose view-model.bind="childFormVM" model.bind="model"></compose>
    <button click.delegate="save()">Save</button>
    <p/>
    <span> Validation Message :  ${model.validateMessage}</span>
  </form>
   <p/>
  <span>Price : ${model.data.price}</span><p/>
  <span>Vat : ${model.data.vat}</span><p/>
  <span>Phone : ${model.data.phone}</span><p/>
  <span>Address : ${model.data.address}</span><p/>
</template>

parentForm.js

import { autoinject } from 'aurelia-framework';

@autoinject
export class ParentForm {
  model = {
   validateMessage : '', 
   data : {
    user : 'My Name'
   }
 };

 childFormVM = 'childForm1';

 validateMessage = '';

 changeChildForm1() {

   this.childFormVM = 'childForm1';
 }

  changeChildForm2() {

   this.childFormVM = 'childForm2';
 }

 save(){
   this.validateRules();
   // How to call the validation rules from child ?
 }

 validateRules (){

   this.model.validateMessage = 'Validate by parent';
 }
}

3 Answers3

3

Bind a function call to the child so that you have a handle to invoke it from the parent. I usually prefer to directly bind the child components rather than using compose, but you can make it work with compose by passing a complex model object rather than only the model, and passing the binding function as one of the model properties.

Parent View-Model:

class Parent {
  model = {};
  child1Validate = null;

  changeChildForm1() {
    if (typeof this.child1Validate === 'function') {
      // the binding was successful; proceed with function call
      let result = this.child1Validate();
      console.log(result);
    }
  }
}

Parent View:

<my-child1 model="parentModel" go-validate="child1Validate"></my-child1>

Child View-Model:

class MyChild1 {
  @bindable model;
  @bindable goValidate;
  bind() {
    // bind the child function to the parent that instantiates the child
    this.goValidate = this.runValidation.bind(this);
  }
  runValidation() {
    // do the validation and pass result to parent...
    return 'Success!';
  }
}
LStarky
  • 2,740
  • 1
  • 17
  • 47
  • I like this solution also. In angular1 I used to do exactly like this, but in Aurelia I didn't knew how to do it. Thanks for the example. I think it's a clear solution for anyone who will create more childs forms in the future. It's clear that child form can have one property that can be called by the parent to validate the business rules. – César Afonso Jul 06 '17 at 21:21
1

That's how you can do it:

parent-form.html

<compose view-model.bind="childFormVM" view-model.ref="childFormInstance" model.bind="model"></compose>

parent-form.js

save() {
  this.childFormInstance.currentViewModel.validateRules();
}

Helpful Notes

Only use <compose> when necessary. For example, in the app.html you should replace <compose> for:

<require from="parentForm"></require>
    
<parent-form></parent-form>

Use kebab-case instead of camel-case to name your files. For example, instead of parentForm.html and parentForm.js, use parent-form.html and parent-form.js. This won't change a thing in your code but you will be following nice javascript standards :)

When binding directly to a string you don't need to use .bind. For example, view-model.bind="'parentForm'" could be replaced for view-model="./parentForm"

Hope this helps!

Community
  • 1
  • 1
Fabio
  • 11,892
  • 1
  • 25
  • 41
  • Ok, this is what I wanted. Thanks. And I will take in account your suggestions as well. – César Afonso Jul 05 '17 at 14:25
  • uhhh.. if `childFormVM` already has the VM, why do they even need to do the ref thing? – Ashley Grant Jul 05 '17 at 16:36
  • @AshleyGrant in his case, `childFormVM` is a string. The ref thing wouldn't be necessary if `childFormVM` was an object. – Fabio Jul 05 '17 at 16:49
  • What do you mean by object? The child form to compose is unknown until runtime when the user chooses which form he will open. – César Afonso Jul 06 '17 at 21:27
  • If `childFormVM` was a view-model instance, you could do something like ``. So in this case, `ref` wouldn't be necessary – Fabio Jul 06 '17 at 23:47
0

One thing that immediately comes to mind is that you can inject the parent model into your child model in the constructor -- the injected instance will be the same, not a newly created one. This way, your parent can define a method that allows the child to register itself on the parent, and the parent can then invoke whatever methods exist on the child at the time of its choosing.

This creates a rather strong coupling between the components, though, so you will need to consider whether or not that is acceptable to you.

If it isn't, another way to approach the issue is to use the event aggregator. The parent form can dispatch an event on the aggregator, and the children will be subscribers listening for the event. In this case, depending on whether or not you host multiple such combinations on one page, you may want to include a unique identifier for the form that is sent along with the event and bind that ID to the child components, so they will know to only listen for events from their parent.

Rytmis
  • 31,467
  • 8
  • 60
  • 69
  • For your first suggestion and if I understand it right, it is the same as using a service that both parent and child have access and where the child can register the function so that the parent can invoke? The second one is async and I need this to be sync. Because the parent can only save data if the model is valid. – César Afonso Jul 05 '17 at 11:25
  • Ah, I see. For the first, you _can_ use a service, but you don't _have to_. If your child component lives inside the parent component in the composed DOM tree, you can simply inject the parent component directly -- that is, you can inject `ParentForm` into the constructor of every `ChildForm`. Another way is to do it via binding, of course, but IMO the constructor approach is better if the coupling really is intentionally that tight. – Rytmis Jul 05 '17 at 17:47