سؤال

Please see this pen for a demo of the issue (based on the slideshow from the tutorial). When clicking on "next" and "prev" arrows, you'll notice that the imgIndex mustache updates correctly, but the expression mustaches such as <p>{{ curImageCaption() }}</p> do not recognize when their values are changing.

That is, the object is mutated such that the mustache value would change if the expressions were re-evaluated, but ractive doesn't seem to realize that. Is there any way to get this to work, barring writing adaptors? Am I misunderstanding how magic mode works? The interesting thing is that even if I explicitly call ractive.update() inside the event handlers, ractive still doesn't respond.

UPDATE WITH NEW INFO

After more fiddling, I came up with this hack that gets it working. The hack is to change, eg, <p>{{ curImageCaption() }}</p> to <p>{{ curImageCaption(imgIndex) }}</p> -- adding a simple primitive to the mustache expression which ractive understands how to watch correctly.

I think I see what's going on now, but having to explicitly add arguments to the mustache expression containing changing primitives defeats much of the purpose of having the separate domain object -- that is, now you are coding your domain object with ractive in mind, using changing primitives a sort of basic pub/sub mechanism for notifying ractive of changes.

Having to create a real pub/sub mechanism on my custom objects, which ractive then explicitly subscribes to, would be fine. The problem is, as I noted in the OP, even when ractive is notified of a change via ractive.update(), it still doesn't know it should recompute the mustaches unless I use the fake argument hack. So it's not clear what callback ractive should be registering to make everything work.

I don't understand the inner-working of ractive well enough to do this, but I suspect what's needed is the ability to directly work with the _deps stuff, and manually trigger recomputes for expressions. If this sounds right, an example of how to accomplish it would be appreciated.

UPDATE 2 -- A decent solution

Here is a proof of concept for a not-too-hacky workaround.

The idea is to use ECMA5 properties to decorate your custom domain object, providing properties that delegate to the existing methods you want to use but which don't work inside ractive templates. The properties, otoh, work just fine.

So instead of <p>{{ curImageCaption() }}</p> we simply write <p>{{ imageCaption }}</p>, and then we decorate our custom domain object like so:

Object.defineProperty(mySlideshow, "imageCaption", {
  configurable: true,
  get: function() { return this.curImageCaption() },
  set: function() { }
});

This decoration, a bit verbose in my demo, can easily be slimmed down by creating a helper method which accepts an object mapping your new ractive-friendly property names to names of existing methods on your object, and takes care of the above boilerplate for you.

NOTE: One drawback of this method is that you do have to call ractive.update() manually in your event handlers. I'd like to know if there's a way of getting around that. And if there is not, how big of a performance hit does this cause? Does it defeat the whole purpose of ractive's surgical updates?

Update 3 -- A better decent solution?

This pen takes yet another approach, in which link our custom domain model with ractive via a generic dispatcher object (an object that implements notify()). I think this is my favorite of the approaches so far....

It's similar to the official ractive adaptors, but we are using DI to pass our unofficial ractive adapter to our domain object, rather than wrapping our object. At first glance it might seem we are "coding to ractive," but in fact this is only partially true. Even if we were using another framework, we'd need to use some notification mechanism to broadcast changes to our view model so that views could react to it. This DI approach seems to require less boilerplate than official ractive adaptors, though I don't understand them well enough to know this for sure. It is not as completely general a solution as the official adaptors either.

Code from pen for posterity

HTML

<div id='output'></div>

<script id='template' type='text/ractive'>
  <div class='slideshow'>
    <div class='main'>
      <a class='prev' on-tap='prev'><span>&laquo;</span></a>
      <div class='main-image' style='background-image: url({{ curImageSrc() }});'></div>
      <a class='next' on-tap='next'><span>&raquo;</span></a>
    </div>

    <div class='caption'>
      <p>{{ curImageCaption() }}</p>
      <p>Image index: {{ imgIndex }} </p>
    </div>
  </div>
</script>

JS

// Fix JS modular arithmetic to always return positive numbers
function mod(m, n) { return ((m%n)+n)%n; }

function SlideshowViewModel(imageData) {
  var self = this;
  self.imgIndex = 0;
  self.next = function() { self.setLegalIndex(self.imgIndex+1); }
  self.prev = function() { self.setLegalIndex(self.imgIndex-1); }
  self.curImage = function() { return imageData[self.imgIndex]; }
  self.curImageSrc = function() { return self.curImage().src; }
  self.curImageCaption = function() { return self.curImage().caption; }
  self.setLegalIndex = function(newIndex) { self.imgIndex = mod(newIndex, imageData.length); } 
}

var mySlideshow = new SlideshowViewModel(
  [
    { src: imgPath('problem.gif'), caption: 'Trying to work out a problem after the 5th hour' },
    { src: imgPath('css.gif'), caption: 'Trying to fix someone else\'s CSS' },
    { src: imgPath('ie.gif'), caption: 'Testing interface on Internet Explorer' },
    { src: imgPath('w3c.gif'), caption: 'Trying to code to W3C standards' },
    { src: imgPath('build.gif'), caption: 'Visiting the guy that wrote the build scripts' },
    { src: imgPath('test.gif'), caption: 'I don\'t need to test that. What can possibly go wrong?' }
  ]
);

var ractive = new Ractive({
  el: '#output',
  template: '#template',
  data: mySlideshow,
  magic: true
});

ractive.on( 'next', function(event) {
  ractive.data.next(); 
});
ractive.on( 'prev', function(event) {
  ractive.data.prev(); 
});


function imgPath(name) { return 'http://learn.ractivejs.org/files/gifs/' + name; }
هل كانت مفيدة؟

المحلول

I'll try and explain what's going on under the hood before presenting a possible solution:

Wrapping objects in magic mode

In magic mode, when Ractive encounters an unwrapped data descriptor of an object, it wraps it by replacing it with an accessor descriptor - the get()/set() functions. (More info on MDN, for those interested.) So when you do self.imgIndex = 1, you're actually triggering the set() function, which knows how to notify all the dependants of the imgIndex property.

The key word here is 'encounters'. The only way Ractive knows that it needs to wrap imgIndex is if we do ractive.get('imgIndex'). This happens internally because we have an {{imgIndex}} mustache.

So that's why the index property updates.

Dependency tracking with computed values

Within an ordinary template, you can have what basically amount to computed values, using the get() method:

<p>{{ curImageCaption() }}</p>
ractive = new Ractive({
  el: 'body',
  template: template,
  data: {
    images: images,
    imgIndex: 0,
    curImageCaption: function () {
      var imgIndex = this.get( 'imgIndex' );
      return this.get( 'images' )[ imgIndex ].caption;
    }
  }
});

Here, because we're calling ractive.get() inside the curImageCaption function, Ractive knows that it needs to rerun the function each time either images or imgIndex changes.

What you're in effect asking is a reasonable question: why doesn't retrieving the value of self.imgIndex in magic mode work the same as doing ractive.get('imgIndex')?

The answer comes in two parts: Firstly, I hadn't thought of adding that feature, and secondly, it turns out it doesn't work! Or rather, it's extremely fragile. I changed magic mode so that the get() accessor captured the dependency the same way ractive.get() does - but self.imgIndex is only an accessor descriptor (as opposed to a data descriptor) if Ractive has already encountered it. So it worked when we had <p>Image index: {{ imgIndex }} </p> at the top of the template, but not when it's at the bottom!

Normally the prescription would be fairly simple: use ractive.get() to make the dependency on self.imgIndex explicit inside curImageSrc() and curImageCaption(). But because you're using a custom viewmodel object, that's not ideal because it effectively means hard-coding keypaths.

A solution - creating a custom adaptor

Here's what I'd recommend - making an adaptor that works with the custom viewmodel object:

Ractive.adaptors.slides = {
  filter: function ( object ) {
    return object instanceof SlideshowViewModel;
  },
  wrap: function ( ractive, slides, keypath, prefix ) {
    var originalNext, originalPrev;

    // intercept next() and prev()
    originalNext = slides.next;
    slides.next = function () {
      originalNext.call( slides );
      ractive.update( keypath );
    };

    originalPrev = slides.prev;
    slides.prev = function () {
      originalPrev.call( slides );
      ractive.update( keypath );
    };

    return {
      get: function () {
        return {
          current: slides.curImage(),
          index: slides.imgIndex
        };
      },
      teardown: function () {
        slides.next = originalNext;
        slides.prev = originalPrev;
      }
    };
  }
};

var ractive = new Ractive({
  el: '#output',
  template: '#template',
  data: mySlideshow,
  adaptors: [ 'slides' ]
});

This is a very simple adaptor, and it could probably be improved, but you get the gist - we're intercepting calls to next() and prev(), and letting Ractive know (via ractive.update()) that it needs to do some dirty checking. Note that we're presenting a facade (via the get() method of the wrapper), so the template looks slightly different - see this pen.

Hope this helps.

نصائح أخرى

Maybe this is an academic exercise, and I'm new to Ractive, but it seems the problem lies in the template not having a context to the current image.

EDITED: Use current Image as a context block instead of looping through collection.

  <div class='slideshow'>
    {{#curImage}}
    <div class='main'>
      <a class='prev' on-tap='prev'><span>&laquo;</span></a>
      <div class='main-image' style='background-image: url({{ src }});'></div>
      <a class='next' on-tap='next'><span>&raquo;</span></a>
    </div>

    <div class='caption'>
      <p>{{ caption }}</p>
      <p>Image index: {{ imgIndex }} </p>
    </div>
  </div>

...

    function SlideshowViewModel(imageData) {
      ...
      self.curImage = imageData[self.imgIndex]
      ...
      self.setLegalIndex = function(newIndex) { 
        self.imgIndex = mod(newIndex,imageData.length); 
        self.curImage = imageData[self.imgIndex]
        } 
    }

This is using your original pen with just the key modifications. Here is new pen.

I would still move the buttons into an outer part of the template so the display in the middle could be made into a partial:

<div class='main'>
  <a class='prev' on-tap='prev'><span>&laquo;</span></a>
  {{#current}}
    {{>partial}}
  {{/}}
  {{/current}}
  <a class='next' on-tap='next'><span>&raquo;</span></a>
</div>

and encapsulate in Ractive.extend, but if ViewModel works for you...

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top