slimjs / slim.js

Fast & Robust Front-End Micro-framework based on modern standards

Home Page:http://slimjs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Model passed in using 'create' plugin does not retain changes

hagen opened this issue · comments

commented

I'm using the approach described here: http://slimjs.com/#/plugins to supply the component with data.
The model is just a JS object.
When my components make changes, these are not being reflected in the model. The components are working with the changes (for example, an s:if condition will react when the value of the property changes), but I don't see that in the original model. Am I missing something?

Here's my usage scenario:

@tag('parent-component')
@template(`
<child-component bind:items="parentProp.path.items"></child-component>
`
export default class ParentComp extends Slim {
  parentProp = {}
}

My child component has a property items and its template contains a list with s:repeat="items as item".
Before the parent-component element is added to DOM, I do:

let data = {
  path: {
    items: [ ... ]
  }
}
Slim.plugin('create', element => {
  if (element.localName === 'parent-component') {
    element.parentProp = data
  }
})

Could it be because the binding to items in the child is too deep? i.e. parentProp.path.items. I have other scenarios where the binding is shallower (e.g. parentProp.prop) and this works.

commented

So it's because I'm binding child elements to nested properties. If you do this, changes to those properties in the child component don't come back up the chain. Is that correct?

Question: Why do you place your own tag into the template? This is like having a recursive element which nests itself endlessly.

Does this works for you?

@tag('parent-component')
@template(`
  <child-component bind:items="parentProp.path.items"></child-component>
export default class ParentComp extends Slim {
  parentProp = {}
}

Regarding bindings. If you make modifications into nested properties, there is no autodetection. Working with immutables is the practice.

If you have to work with nested changes, call the ::this.commit(propName) which will trigger as if it detected the change.

commented

The parent tag shouldn't be there. Bad, bad example! Sorry to confuse the issue.

Re bindings, this is the PAP approach right? If you have to bind to nested props, then use another (child) Slim component. Each level of nesting in my model should have a corresponding child Slim component that'll bind at that level. I think I've got it now.

commented

I can't get my head around this.
I have this structure:

@tag('parent-component')
@template(`
<div>
  <child-component bind:items="data.items"></child-component>
</div>
`
export default class ParentComp extends Slim {
  data = {}
}

And to this parent component, I inject the data model at 'create'. This model has a collection, items.

let data = {
  "items": [
    { "label": "First", "editing": false },
    { "label": "Second", "editing": false }
  ]
}

Injected:

Slim.plugin('create', element => {
  if (element.localName === 'parent-component') {
    element.data = data
  }
})

Now the child component

@tag('child-component')
@template(`
  <div>
    <h3>Title</h3>    
    <table>
      <tbody>
        <tr>
          <th>Header</th>
        </tr>
        <tr s:repeat="items as item">
          <th>
            <editable-link bind:label="item.label" bind:editing="item.editing"></editable-link>
          </th>
        </tr>
      </tbody>
    </table>
  </div>
`)
export default class ChildComponent extends Slim {
  items = []
}

And finally, the editable-link component (this is just an anchor/input toggle to allow editing of the anchor by clicking it. I'm using key.enter and key.escape as directives (thanks for that excellent example!)

@tag('editable-link')
@template(`
  <input s:if="editing" key.enter="onEnterPress" key.escape="onEscapePress" type="text" class="input" bind:value="label" autofocus>
  <a s:if="!editing" click="onLinkClick" bind>{{label}}</a>
`)
export default class EditableLink extends Slim {
  label
  editing
  onLinkClick() {
    this.editing = true
  }
  onEnterPress(target) {
    this.editing = false
    this.label = target.value
    // Trigger binding change?
    this.commit('label')
  }
}

What I'm expecting is that any changes down in editable-link to the item labels will be reflected back up in the data model injected into parent-component. Is this an accurate understanding? If not, how would I get the changed label values from editable-link back up into the data model?

commented

For now, I've worked around my lack of understanding using this.callAttribute() to pass data back up to parent elements. This eventually reaches the parent-component, where I replace the entire data property.

So I understand everything works as expected now?

commented

Not as expected, but I've got it working with what I think is a work-around. I'm just seeking clarification as to how I correctly approach implementation of my scenario in Slim?

The binding works locally on each component (repeated in your case). You should propagate it up either by event or by using callAttribute which is just a callback in an attribute... :)

I think that binding that component::label into the input::value may be incorrect in your case, try using the <input type="text" ... input="handleInput" ... /> which will trigger you function on every change in the text-input. This way you can capture the value as the user types. Capturing the label from the input and passing it back to the input seems like a no-op.

Makes sense?

commented

Yep, thanks. The callAttribute is super useful then, as it's almost the way to propagate up. I know you're doing more work on the doco - this seems a good one to include. I picked it up from looking through slim-docs repo.
Closing.

The best practice in web components is to dispatch events. In case of custom elements, probably using shadow-dom, you should dispatch a custom event with an init object with composed = true.

this.dispatchEvent(new CustomEvent('myaction', { bubbles: true, composed: true, detail: whatever });