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

Add x-model support if variable is not defined in x-data #3798

Closed
wants to merge 9 commits into from
33 changes: 25 additions & 8 deletions packages/alpinejs/src/directives/x-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
})
}
}

if (typeof expression === 'string' && el.type === 'radio') {
// Radio buttons only work properly when they share a name attribute.
// People might assume we take care of that for them, because
Expand All @@ -59,22 +59,39 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
// If the element we are binding to is a select, a radio, or checkbox
// we'll listen for the change event instead of the "input" event.
var event = (el.tagName.toLowerCase() === 'select')
|| ['checkbox', 'radio'].includes(el.type)
|| modifiers.includes('lazy')
? 'change' : 'input'
|| ['checkbox', 'radio'].includes(el.type)
|| modifiers.includes('lazy')
? 'change' : 'input'

// We only want to register the event listener when we're not cloning, since the
// mutation observer handles initializing the x-model directive already when
// the element is inserted into the DOM. Otherwise we register it twice.
let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
setValue(getInputValue(el, modifiers, e, getValue()))
})

if (modifiers.includes('fill'))
if ([null, ''].includes(getValue())
|| (el.type === 'checkbox' && Array.isArray(getValue()))) {

if (modifiers.includes('fill')) {
// autofill x-data object
if (typeof expression === 'string') {
let value = '',
xData = Alpine.$data(el);

const [key, ...rest] = expression.split('.');
if (el.type === 'checkbox') {
value = typeof xData[key] === 'undefined' ? '' : Array.from(xData[key]);
} else if (el.type === 'radio') {
value = el.checked ? el.value : '';
} else {
value = xData[key] ?? '';
}
xData[key] = rest.length > 0 ? rest.reduceRight((acc, key) => ({[key]: acc}), value) : value;
}

if ([null, ''].includes(getValue()) || (el.type === 'checkbox' && Array.isArray(getValue()))) {
el.dispatchEvent(new Event(event, {}));
}
}

// Register the listener removal callback on the element, so that
// in addition to the cleanup function, x-modelable may call it.
// Also, make this a keyed object if we decide to reintroduce
Expand Down
6 changes: 4 additions & 2 deletions packages/docs/src/en/directives/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,13 @@ The default throttle interval is 250 milliseconds, you can easily customize this

By default, if an input has a value attribute, it is ignored by Alpine and instead, the value of the input is set to the value of the property bound using `x-model`.

But if a bound property is empty, then you can use an input's value attribute to populate the property by adding the `.fill` modifier.
But if a bound property is not declared, then you can use an input's value attribute to populate the property by adding the `.fill` modifier. Alpine will take over the formation of the data object, which eliminates the need to declare properties separately.

<div x-data="{ message: null }">
```alpine
<div x-data>
<input x-model.fill="message" value="This is the default message.">
</div>
```

<a name="programmatic access"></a>
## Programmatic access
Expand Down
83 changes: 83 additions & 0 deletions tests/cypress/integration/directives/x-model.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,86 @@ test(
}
);

test(
'x-model with fill modifier add autofill x-data object (no need fill x-data)',
html`
<div x-data>
<div>
Nested property
<input type="text" x-model.fill="post.data.name" placeholder="Enter text" value="Post ID">
<span x-text="post.data.name"></span>
</div>
<br>
<div>
Text
<input type="text" x-model.fill="test" placeholder="Enter text" value="String text input">
<span x-text="test"></span>
</div>
<br>
<div>
Number
<input type="number" x-model.fill="years" placeholder="Enter number" value="55">
<span x-text="years"></span>
</div>
<br>
<div>
Single checkbox with boolean
<input type="checkbox" value="red" x-model.fill="cb" checked>
<span x-text="cb"></span>
</div>
<br>
<div>
Multiple checkboxes bound to array
<input type="checkbox" value="red" x-model.fill="colors">
<input type="checkbox" value="orange" x-model.fill="colors" checked>
<input type="checkbox" value="yellow" x-model.fill="colors" checked>
<span x-text="colors"></span>
</div>
<br>
<div>
Radio Button
<input type="radio" value="rb1" x-model.fill="radio"> Radio Button 1
<input type="radio" value="rb2" x-model.fill="radio" checked> Radio Button 2
<span x-text="radio"></span>
</div>
<br>
<div>
Textarea
<textarea x-model.fill="textarea">String textarea content</textarea>
<span x-text="textarea"></span>
</div>
<br>
<div>
Select
<select x-model.fill="color">
<option value="">Choose color</option>
<option value="red">Red</option>
<option value="orange">Orange</option>
<option value="yellow" selected>Yellow</option>
</select>
Color: <span x-text="color"></span>
</div>
<br>
<div>
Multiple Select
<select x-model.fill="colorList" multiple="">
<option value="red">Red</option>
<option value="orange" selected>Orange</option>
<option value="yellow" selected>Yellow</option>
</select>
Color: <span x-text="colorList"></span>
</div>
</div>
`,
({ get }) => {
get('[x-data]').should(haveData('post', {data: {name: 'Post ID'}}));
get('[x-data]').should(haveData('test', 'String text input'));
get('[x-data]').should(haveData('years', '55'));
get('[x-data]').should(haveData('cb', true));
get('[x-data]').should(haveData('colors', ['orange', 'yellow']));
get('[x-data]').should(haveData('radio', 'rb2'));
get('[x-data]').should(haveData('textarea', 'String textarea content'));
get('[x-data]').should(haveData('color', 'yellow'));
get('[x-data]').should(haveData('colorList', ['orange', 'yellow']));
}
);