I have the following template
<script type="text/x-handlebars" id="friends/new">
<label>First Name</label>
{{input value=firstName}}<br />
<label>Last Name</label>
{{input value=lastName}}<br />
<label>About</label>
{{textarea value=about}}<br />
<button {{action "create"}} {{bind-attr disabled=isInvalid}}>Create</button>
</script>
I put data into all the fields and click the create button, which goes to the following controllers
App.FriendsNewRoute = Ember.Route.extend({
model: function(){
return { firstName: "", lastName: "", about: ""}
}
});
App.FriendsNewController = Ember.Controller.extend({
needs: "friends",
isInvalid: true,
validForm: function(){
if(this.get('lastName') && this.get('firstName')){
this.set("isInvalid", false);
} else {
this.set("isInvalid", true);
}
}.observes('firstName','lastName'),
actions: {
create: function(){
var newFriend = Ember.copy(this.content);
console.log(newFriend);
}
}
});
When calling this.get('lastName') ect the content that I have entered into the text boxes is correct. But when I log this.content, the value is still the initial value that I set in the FriendsNewRoute. What do I need to do so that this.content correctly update with the current data in my template?
You should change:
Ember.Controller
to
Ember.ObjectController
Related
I'm trying to bind focusout event to my knockout js. here is the example:
<div class="form">
<label>
Country:
</label>
<input type="text" id="countryName" name="countryId._autocomplete" data-bind="value: countryName,event: { blur: onBlurCountryEvent }" />
</div>
<div class="form" data-bind="visible: onBlurCountryEvent">
<label>
Time zone:
</label>
<input type="text" id="timeZoneName" name="timeZoneId._autocomplete" data-bind="value: timeZoneName" />
</div>
and this is my knockoutjs:
define(['viewmodels/shell', 'durandal/system', 'durandal/services/logger', 'plugins/router', 'knockout', 'common', 'jqueryform', 'toastr', 'kovalidationconfig'],
function (shell, system, logger, router, ko, common, jqueryform, toastr, kvc) {
var vm = {
activate: activate,
logger: logger,
shell: shell,
countryId: ko.observable(),
countryName: ko.observable(),
timeZoneName: ko.observable(),
timeZoneId: ko.observable(),
timeZoneVisibility: timeZoneVisibility,
bindingComplete: function (view) {
bindFindCountryEvent(view);
bindFindTimeZoneEvent(view);
}
};
vm.onBlurCountryEvent = function () {
var countryVal = $('#countryName').val();
if (countryVal != undefined && countryVal != null && countryVal != '') {
console.log("trueee");
return true;
}
else {
console.log("falseee");
return false;
}
}
function bindFindCountryEvent(view) {
jQuery("#countryName", view).typeahead(
...
}
function bindFindTimeZoneEvent(view) {
jQuery("#timeZoneName", view).typeahead(
...
}
function activate(id) {
shell.isLoading(true);
...
shell.isLoading(false);
});
return true;
}
vm.save = function () {
...
};
});
So, as you can see, I want to have some event and binded function, when I do onBlur from my field country, to check, and to preview timezone field if there any selected country from dropdown search.
Also, if user skips the country, timezone filed should remain visible:false
the event works, and I can see in my console true/false values.
However, my field of timeZone is intact. No matter if this country field is empty or non-empty, the fields is visible always.
If I put visible:false (hardcoded value), it works.
Should I need to bind that function vm.onBlurCountryEvent?
the problem is that the function onBlurCountryEvent is not an observable, so knockout is not checking for changes. I would suggest adding a isTimezoneVisible : ko.observable(false) to your view model then set the isTimeZoneVisible in the onBlurCountryEvent.
In your view set the visible binding to isTimeZoneVisible. Something like the following
var vm = {
countryId: ko.observable(),
countryName: ko.observable(),
timeZoneName: ko.observable(),
timeZoneId: ko.observable(),
isTimeZoneVisible: ko.observable(false), //new property
bindingComplete: function(view) {
bindFindCountryEvent(view);
bindFindTimeZoneEvent(view);
}
};
vm.onBlurCountryEvent = function() {
var countryVal = $('#countryName').val();
if (countryVal != undefined && countryVal != null && countryVal != '') {
console.log("trueee");
vm.isTimeZoneVisible(true); //set property
} else {
console.log("falseee");
vm.isTimeZoneVisible(false); //set property
}
}
function bindFindCountryEvent(view) {
}
function bindFindTimeZoneEvent(view) {
}
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="form">
<label>
Country:
</label>
<input type="text" id="countryName" name="countryId._autocomplete" data-bind="value: countryName,event: { blur: onBlurCountryEvent }" />
</div>
<div class="form" data-bind="visible: isTimeZoneVisible">
<label>
Time zone:
</label>
<input type="text" id="timeZoneName" name="timeZoneId._autocomplete" data-bind="value: timeZoneName" />
</div>
I need to display a list to select an element. By selecting an element I want to show a edit form. Therefore I'm using reactiveVar, but I don't get the id of the selected element to the edit form / helper to get all needed data.
Maybe this is bad coding and there is a much better way to switch between a list and a display for the selected element!
Users is just taken as an example. Could be any collection/data
templates
<template name="users">
{{#if userId}}
{{ > userEdit userId}}
{{else}}
<h1>List</h1>
<div id="usersList">
<ul>
{{#each users}}
<li data-id="{{_id}}">{{users.name}}</li>
{{/each}}
</ul>
</div>
{{/if}}
</template>
<template name="userEdit">
<h1>Edit User</h1>
<form id="userEdit" data-id="{{user._id}}">
<label><input type="text" name="name" value="{{user.name}}"> Name</label>
</form>
</template>
helpers
Template.users.helpers({
users: function() { return Users.find({}) },
userId: function() { return Template.instance().userId.get(); }
});
Template.userEdit.helpers({
user: function() { return Users.findOne({ _id: Template.instance().parentView.userId.get() }); }
});
events
Template.users.onCreated(function() {
this.userId = new ReactiveVar();
});
Template.users.events({
'click #usersList li': function(event, template) {
var $this = $(event.currentTarget),
id = $this.attr('data-id');
if (id)
template.userId.set(id);
}
});
First of all, you can't use ReactiveVar for cross template. Use Session instead. Try this and let me know:
Template.users.events({
'click #usersList li': function(event, template) {
Session.set('userId', this._id)
console.log(Session.get('userId'));
}
});
Template.userEdit.helpers({
user: function() {
return Users.findOne({ _id: Session.get('userId')});
}
});
I'm trying to create very simple login with backbonejs. Collection stores usernames and passwords. Login view has two inputs and on click it should perform check function and compare input value with data from collection.
Html part looks like this:
<div class="login-block">
<script type="text/template" id="start">
<form id="login">
<div class="input-wrapper"><input type="text" placeholder="Username" id="username" required></div>
<div class="input-wrapper"><input type="password" placeholder="Password" id="password" required></div>
<div class="input-wrapper"><button class="btn">Sign in!</button></div>
</form>
</script>
<div class="error" class="block">
Error
</div>
<div class="success">
Success
</div>
</div>
Here is my Js code:
var User = Backbone.Model.extend({
defaults: {
login: 'root',
mail: 'root#mail.com',
password: ''
}
});
var user = new User();
//variable to store username
var loginData = {
username: "",
password: ""
}
// userbase
var UserCollection = Backbone.Collection.extend({
model: User
});
var userCollection = new UserCollection([
{
username: 'Ivan',
mail: 'ivan#mail.com',
password: '1234'
},
{
username: 'test',
mail: 'test#mail.com',
password: 'test'
}
]);
// login page
var LoginView = Backbone.View.extend({
el: $(".login-block"),
events: {
"click .btn": "check"
},
check: function(){
loginData.username = this.$el.find("#username").val(); // store username
loginData.password = this.$el.find("#password").val();// store password
if (loginData.username === userCollection.each.get("username") && loginData.password === userCollection.each.get("password"))
{appRouter.navigate("success", {trigger: true});
}else{
appRouter.navigate("error", {trigger: true});
}
},
render: function () {
//$(this.el).html(this.template());
var template = _.template($('#start').html())
$(this.el).html(template());
//template: template('start');
return this;
}
});
var loginView = new LoginView({collection: userCollection});
var AppRouter = Backbone.Router.extend({
routes: {
'': 'index', // start page
'/error': 'error',
'/success': 'success'
},
index: function() {
loginView.render();
console.log("index loaded");
},
error: function(){
alert ('error');
},
success: function(){
console.log('success');
}
});
var appRouter = new AppRouter();
Backbone.history.start();
It works fine to the check function, and it stores username and password, but something is clearly wrong either with router or check function when it starts comparison. Instead of routing to success or error page, it rerenders index page.
P.S I didn't use namespacing and code in general is not of a greatest quality, but it was made for educational purpose only.
You have to add the attribute type="button" to your button, otherwise it will submit the form when clicked (See this question):
<script type="text/template" id="start">
<form id="login">
<div class="input-wrapper"><input type="text" placeholder="Username" id="username" required></div>
<div class="input-wrapper"><input type="password" placeholder="Password" id="password" required></div>
<div class="input-wrapper"><button class="btn" type="button">Sign in!</button></div>
</form>
</script>
You can also return false in the click event handler, which would cancel the default action. (submitting the form, if you don't add type="button").
For comparing the values with the hardcoded collection, you can't call each as you where doing (which is an iteration function provided by Underscore) because you would receive an error. You could use Underscore's findWhere method which is also available in Backbone collections. So the click event handler (Your check function) could look like this:
check: function(){
loginData.username = this.$el.find("#username").val(); // store username
loginData.password = this.$el.find("#password").val();// store password
if(userCollection.findWhere({username: loginData.username, password: loginData.password})){
appRouter.navigate("success", {trigger: true});
}else{
appRouter.navigate("error", {trigger: true});
}
return false;
},
You can try it on this fiddle
The logic check you're doing doesn't look like it would work to me. I would expect the following to generate an error:
userCollection.each.get('username')
the function you're calling on your collection, each, is a wrapped underscore method which takes a function callback as a parameter. If you want to check your username and password, I'd do something like this:
var user = userCollection.findWhere({ username: loginData.userName });
This will return you the model where the username matches. Then you can check the password of that model:
if (user.get('password') === loginData.password) {
// do something
} else {
// do something else
}
EDIT Heck, you can do both checks at once:
var user = userCollection.findWhere({ username: loginData.userName, password: loginData.password });
I'll leave the previous code up just to demonstrate.
I'm building a quiz generator as my first Ember project, but I'm struggling. I've been able to create, edit and delete quizzes and save them to localstorage, but I'm having trouble saving/deleting quiz questions for each quiz.
I'm building it in Yeoman-Ember. I tried to add a demo to JSBin but it didn't work, so I've got a demo build here:
http://test.richardwestenra.com/embertest/
and here's a zip containing the current state of the build: http://test.richardwestenra.com/embertest/dev.zip
Here's my combined-scripts.js file:
(function() {
var Quizmaker = window.Quizmaker = Ember.Application.create();
/* Order and include as you please. */
})();
(function() {
/* global $ */
Quizmaker.QuizzesController = Ember.ObjectController.extend({
});
Quizmaker.NewController = Ember.ObjectController.extend({
content: {},
quiztypes: ['Multiple choice', 'List', 'Either/or'],
actions: {
save: function(){
var title = $('#title').val();
var excerpt = $('#excerpt').val();
var quiztype = $('#quiztype').val();
var fullname = $('#fullname').val();
var submittedOn = new Date();
var store = this.get('store');
if (Ember.isEmpty(title)) {
window.alert('Please enter a title');
return false;
} else if (Ember.isEmpty(quiztype)) {
window.alert('Please enter a quiz type');
return false;
}
var quiz = store.createRecord('quiz',{
quiztype : quiztype,
fullname : fullname,
title : title,
excerpt : excerpt,
submittedOn : submittedOn
});
quiz.save();
this.transitionToRoute('index');
},
cancel: function(){
this.transitionToRoute('index');
},
createQuestion: function(){
window.alert('This doesn\'t work for new questions. I don\'t know why. It works for existing questions.');
var store = this.get('store');
var question = store.createRecord('question',{
question : 'Test question ' + new Date()
});
var model = this.get('model');
var questions = this.get('questions');
questions.pushObject(question);
model.set('questions', questions);
model.save();
}
}
});
Quizmaker.QuizController = Ember.ObjectController.extend({
quiztypes: ['Multiple choice', 'Checklist', 'Boolean'],
actions: {
edit: function(){
this.transitionToRoute('quiz.edit');
this.set('isEditing', true);
},
doneEditing: function(){
var model = this.get('model');
var title = $('#title').val();
var excerpt = $('#excerpt').val();
var quiztype = $('#quiztype').val();
var fullname = $('#fullname').val();
var questions = this.get('questions');
if (Ember.isEmpty(title)) {
window.alert('Please enter a title');
return false;
} else if (Ember.isEmpty(quiztype)) {
window.alert('Please enter a quiz type');
return false;
} else {
this.set('isEditing', false);
model.set('title', title);
model.set('excerpt', excerpt);
model.set('quiztype', quiztype);
model.set('fullname', fullname);
model.set('questions', questions);
model.save();
this.transitionToRoute('quiz');
}
},
cancel: function(){
if (window.confirm('Are you sure you want to abandon your changes?')){
this.set('isEditing', false);
this.transitionToRoute('quiz');
}
},
remove: function(){
if (window.confirm('Are you sure you want to delete this quiz?')){
var quiz = this.get('model');
quiz.destroyRecord();
this.transitionToRoute('index');
}
},
createQuestion: function(){
var store = this.get('store');
var question = store.createRecord('question',{
question : 'Test question ' + new Date()
});
var model = this.get('model');
var questions = this.get('questions');
questions.pushObject(question);
model.set('questions', questions);
model.save();
}
}
});
Quizmaker.QuestionsController = Ember.ArrayController.extend({
needs: 'quiz',
quiz: Ember.computed.alias("controllers.quiz"),
actions: {
createQuestion: function(){
var store = this.get('store');
var question = store.createRecord('question',{
question : 'Test question ' + new Date()
});
var quiz = this.get('quiz');
var questions = quiz.get('questions');
questions.pushObject(question);
console.log(question);
},
removeQuestion: function(id){
var question = this.findProperty('id', id);
this.removeObject(question);
}
}
});
})();
(function() {
Quizmaker.Store = DS.Store.extend();
// Quizmaker.ApplicationAdapter = DS.FixtureAdapter;
Quizmaker.ApplicationAdapter = DS.LSAdapter.extend({
namespace: 'quizzes'
});
})();
(function() {
/* global Ember */
Quizmaker.Quiz = DS.Model.extend({
title : DS.attr('string'),
excerpt : DS.attr('string'),
fullname : DS.attr('string'),
quiztype : DS.attr('string'),
questions: DS.hasMany('question', {async: true}), // via http://stackoverflow.com/questions/22494140/in-ember-js-how-do-i-create-a-computed-property-that-references-first-item-in-pr
questionsCount: function() {
return this.get('questions.length');
}.property('questions.#each'), // via http://stackoverflow.com/questions/16463958/how-to-use-multiple-models-with-a-single-route-in-emberjs-ember-data
// firstQuestion: function() {
// return this.get('questions.firstObject');
// }.property('questions.firstObject')
submittedOn : DS.attr('date')
});
Quizmaker.Question = DS.Model.extend({
quiz: DS.belongsTo('quiz'),
question: DS.attr('string'),
answers: DS.attr('string')
});
// delete below here if you do not want fixtures
Quizmaker.Quiz.FIXTURES = [
{
id: 0,
title: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
excerpt: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.',
quiztype: 'Boolean',
fullname: 'Full Name',
submittedOn: null
}
];
Quizmaker.Question.FIXTURES = [
{
id: 0,
question: 'Test question Lorem ipsum dolor sit amet, consectetur adipiscing elit',
quiz: 0,
answers: [
{ answer: 'alpha', weight: 0 },
{ answer: 'beta', weight: 5 }
]
}
];
})();
(function() {
Quizmaker.ApplicationRoute = Ember.Route.extend({
model: function () {
return this.get('store').findAll('quiz');
}
});
Quizmaker.QuizzesRoute = Ember.Route.extend({
model: function() {
return this.get('store').find('quiz');
}
});
Quizmaker.QuizRoute = Ember.Route.extend({
model: function(params) {
return this.get('store').find('quiz', params.quiz_id);
}
});
Quizmaker.QuestionsRoute = Ember.Route.extend({
controllerName: 'quiz',
model: function() {
return this.store.findAll('question');
}
});
})();
(function() {
Quizmaker.QuizView = Ember.View.extend({
keyDown: function(e) {
var esc = 27;
if(e.keyCode === esc){
this.get('controller').send('cancel');
}
}
});
// Give the text fields one way value binding so they don't automatically update
Quizmaker.TextField = Ember.TextField.extend({
valueBinding: Ember.Binding.oneWay('source')
});
Quizmaker.TextArea = Ember.TextArea.extend({
valueBinding: Ember.Binding.oneWay('source')
});
Quizmaker.Select = Ember.Select.extend({
valueBinding: Ember.Binding.oneWay('source')
});
})();
(function() {
Quizmaker.QuizzesView = Ember.View.extend({
});
})();
(function() {
Quizmaker.Router.map(function () {
// Add your routes here
this.resource('index',{path : '/'});
this.resource('new' , {path : '/quiz/new'});
this.resource('quizzes' , {path : '/quizzes'});
this.resource('questions' , {path : '/questions'});
this.resource('quiz', { path: '/quiz/:quiz_id' }, function(){
this.route('edit', { path: '/edit' });
});
});
})();
(function() {
/* global moment */
Ember.Handlebars.helper('format-date', function(date){
return moment(date).fromNow();
});
})();
This is quiz.hbs:
{{#if isEditing}}
<h1>Edit quiz</h1>
{{partial "editform"}}
{{else}}
<h1>{{title}}</h1>
<h4>A <em style="text-transform: lowercase;">{{quiztype}}</em> quiz by {{fullname}} <small class="muted">{{format-date submittedOn}}</small></h4>
<hr>
<p class="lead">{{excerpt}}</p>
<button type="submit" class="btn btn-default" {{action 'edit'}}>Edit</button>
<button class="btn btn-danger pull-right" {{action 'remove'}}>Delete Quiz</button>
{{/if}}
This is _editform.hbs:
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">Title</label>
<div class="col-sm-10">
{{!-- {{#view Quizmaker.TextInputView}}
{{/view}} --}}
{{!-- <input type="text" class="form-control" id="title" name="title" placeholder="Title of the quiz" required> --}}
{{view Quizmaker.TextField type="text" class="form-control" id="title" name="title" sourceBinding="title" placeholder="Title of the quiz" required="required" }}
</div>
</div>
<div class="form-group">
<label for="excerpt" class="col-sm-2 control-label">Excerpt</label>
<div class="col-sm-10">
{{!-- <textarea class="form-control" id="excerpt" name="excerpt" placeholder="Short description of the quiz" required></textarea> --}}
{{view Quizmaker.TextArea class="form-control" id="excerpt" name="excerpt" sourceBinding="excerpt" placeholder="Short description of the quiz" rows="3" required="required" }}
</div>
</div>
<div class="form-group">
<label for="fullname" class="col-sm-2 control-label">Author</label>
<div class="col-sm-10">
{{!-- <input type="text" class="form-control" id="fullname" name="fullname" placeholder="Enter your Full Name like Alan Smithee" required> --}}
{{view Quizmaker.TextField type="text" class="form-control" id="fullname" name="fullname" sourceBinding="fullname" placeholder="Enter your full name, e.g. Alan Smithee" required="required" }}
</div>
</div>
<div class="form-group">
<label for="quiztype" class="col-sm-2 control-label">Quiz type</label>
<div class="col-sm-10">
{{view Quizmaker.Select id="quiztype" name="quiztype" class="form-control" viewName="select" content=quiztypes prompt="Pick a type:" sourceBinding="quiztype"}}
</div>
</div>
<div class="form-group">
<label for="quiztype" class="col-sm-2 control-label">Questions ({{questionsCount}})</label>
<div class="col-sm-10">
{{render "questions" questions}}
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
{{#if isEditing}}
<button type="submit" class="btn btn-success" {{action 'doneEditing'}}>Save</button>
<button class="btn btn-warning pull-right" {{action 'cancel'}}>Cancel</button>
{{else}}
<button type="submit" class="btn btn-success" {{action 'save'}}>Save</button>
<button class="btn btn-warning pull-right" {{action 'cancel'}}>Cancel</button>
{{/if}}
</div>
</div>
</form>
And this is questions.hbs:
<p><button class="btn btn-info btn-sm" {{action 'createQuestion'}}><span class="glyphicon glyphicon-plus"></span> Add new question</button></p>
<table class="table table-striped">
{{#each model}}
<tr>
<td>{{this.question}}</td>
<td>{{this.id}}</td>
<td><button class="btn btn-danger btn-xs pull-right" {{action 'removeQuestion' id}}>Delete</button></td>
</tr>
{{/each}}
</table>
If anyone can point out ways I can improve it, I'd be very grateful. I've tried implementing every method I can find to get it to save or delete hasMany data but it always throws different errors and I'm not really sure where to proceed from here.
I'm also trying to figure out how to toggle the isEditing state depending on whether the /edit route is displayed (instead of having it activated by an action).
let's break this down into two different items, last to first.
The trick in showing the edit mode versus view mode is to take advantage of the free index route which is rendered at the root of every resource.
You can change your quiz template to be an outlet
{{outlet}}
and create a quiz/index template which originally held your quiz data. This template will only show up when you are at /quiz/123 when you navigate to /quiz/123/edit the index template will be replaced with the edit template. (You'll probably want to return the model from the resource to both routes using modelFor).
App.Router.map(function() {
this.resource('foo', {path:'/'},function(){
this.route('edit');
})
});
App.FooRoute = Em.Route.extend({
model: function() {
return [{color:'red'}, {color:'yellow'},{color: 'blue'}];
}
});
App.FooIndexRoute = Ember.Route.extend({
model: function() {
return this.modelFor('foo');
}
});
App.FooEditRoute = Ember.Route.extend({
model: function() {
return this.modelFor('foo');
}
});
http://emberjs.jsbin.com/lodeviyu/1/edit
In Ember Data if you have two record types that each have a relation to each other:
App.FooRecord = DS.Record.extend({
bars: DS.hasMany('bar')
});
App.BarRecord = DS.Record.extend({
foo: DS.belongsTo('foo')
});
Let's create and associate two records:
var foo = store.create('foo');
var bar = store.create('bar');
foo.get('bars').pushObject(bar);
bar.set('foo', foo);
Now let's save, and see what Ember Data will do:
foo.save();
// json sent
{
foo: {}
}
// responds with id 1
bar.save();
// json sent
{
bar: {
foo: 1
}
}
Ember Data chooses not to save the hasMany relationship if their is a correlated belongsTo from a different model type.
https://github.com/emberjs/data/commit/7f752ad15eb9b9454e3da3f4e0b8c487cdc70ff0#commitcomment-6078838
I want to add a data attribute (data-invalid) to a text field if it is invalid and remove it if it is valid. Here's the html and js:
<form data-bind="submit: submitFields">
First Name: <input type="text" runat="server" ID="FirstName" data-bind="value: firstName, valueUpdate: 'blur', attr: { 'data-invalid': !firstName.isValid()}"/>
<br/>
<button type="submit" >Submit</button>
</form>
var viewModel;
viewModel = function() {
self = this;
self.firstName = ko.observable().extend({
required: true,
notify: 'always'
});
//sets errors to an array of the error messages?
self.errors = ko.validation.group(this, { deep: true, observable: false })
self.submitFields = function(formElement) {
if (self.errors().length > 0) {
self.errors.showAllMessages();
return;
}
//submit
}
};
ko.applyBindings(new viewModel());
And a fiddle to test it out:
http://jsfiddle.net/frontenderman/HhtEZ/2/
My problem is that firstName.IsValid() runs on pageload. Is there an elegant way to get around this? All I can think of is doing something like subscribing to firstName and using a flag to set the data-attribute to null on the first run through my view model and then returning !firstName.isValid() on any of the subsequent runs.